diff --git a/backend/config/__init__.py b/backend/config/__init__.py
index e4ba1e0cd..8b48bd1fd 100644
--- a/backend/config/__init__.py
+++ b/backend/config/__init__.py
@@ -108,6 +108,13 @@ def str_to_bool(value: str) -> bool:
"SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON",
"0 4 * * *", # At 4:00 AM every day
)
+ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA: Final = str_to_bool(
+ os.environ.get("ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA", "false")
+)
+SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON: Final = os.environ.get(
+ "SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON",
+ "0 5 * * *", # At 5:00 AM every day
+)
# EMULATION
DISABLE_EMULATOR_JS = str_to_bool(os.environ.get("DISABLE_EMULATOR_JS", "false"))
diff --git a/backend/endpoints/heartbeat.py b/backend/endpoints/heartbeat.py
index 44b7221e4..9bd2ca62b 100644
--- a/backend/endpoints/heartbeat.py
+++ b/backend/endpoints/heartbeat.py
@@ -4,11 +4,13 @@
DISABLE_USERPASS_LOGIN,
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
ENABLE_SCHEDULED_RESCAN,
+ ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB,
OIDC_ENABLED,
OIDC_PROVIDER,
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
SCHEDULED_RESCAN_CRON,
+ SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON,
UPLOAD_TIMEOUT,
)
@@ -64,6 +66,12 @@ def heartbeat() -> HeartbeatResponse:
"TITLE": "Scheduled Switch TitleDB update",
"MESSAGE": "Updates the Nintendo Switch TitleDB file",
},
+ "LAUNCHBOX_METADATA": {
+ "ENABLED": ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
+ "CRON": SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
+ "TITLE": "Scheduled LaunchBox metadata update",
+ "MESSAGE": "Updates the LaunchBox metadata",
+ },
},
"EMULATION": {
"DISABLE_EMULATOR_JS": DISABLE_EMULATOR_JS,
diff --git a/backend/endpoints/responses/heartbeat.py b/backend/endpoints/responses/heartbeat.py
index f44323f5d..765e44754 100644
--- a/backend/endpoints/responses/heartbeat.py
+++ b/backend/endpoints/responses/heartbeat.py
@@ -19,6 +19,7 @@ class TaskDict(WatcherDict):
class SchedulerDict(TypedDict):
RESCAN: TaskDict
SWITCH_TITLEDB: TaskDict
+ LAUNCHBOX_METADATA: TaskDict
class MetadataSourcesDict(TypedDict):
diff --git a/backend/endpoints/tasks.py b/backend/endpoints/tasks.py
index 12f301a22..4470d82b6 100644
--- a/backend/endpoints/tasks.py
+++ b/backend/endpoints/tasks.py
@@ -2,6 +2,7 @@
from endpoints.responses import MessageResponse
from fastapi import Request
from handler.auth.constants import Scope
+from tasks.update_launchbox_metadata import update_launchbox_metadata_task
from tasks.update_switch_titledb import update_switch_titledb_task
from utils.router import APIRouter
@@ -19,12 +20,13 @@ async def run_tasks(request: Request) -> MessageResponse:
"""
await update_switch_titledb_task.run()
+ await update_launchbox_metadata_task.run()
return {"msg": "All tasks ran successfully!"}
@protected_route(router.post, "/tasks/{task}/run", [Scope.TASKS_RUN])
async def run_task(request: Request, task: str) -> MessageResponse:
- """Run all tasks endpoint
+ """Run single tasks endpoint
Args:
request (Request): Fastapi Request object
@@ -32,7 +34,10 @@ async def run_task(request: Request, task: str) -> MessageResponse:
RunTasksResponse: Standard message response
"""
- tasks = {"switch_titledb": update_switch_titledb_task}
+ tasks = {
+ "switch_titledb": update_switch_titledb_task,
+ "launchbox_metadata": update_launchbox_metadata_task,
+ }
await tasks[task].run()
return {"msg": f"Task {task} run successfully!"}
diff --git a/backend/endpoints/tests/test_heartbeat.py b/backend/endpoints/tests/test_heartbeat.py
index d8ac46449..d1564d168 100644
--- a/backend/endpoints/tests/test_heartbeat.py
+++ b/backend/endpoints/tests/test_heartbeat.py
@@ -27,4 +27,12 @@ def test_heartbeat(client):
heartbeat.get("SCHEDULER").get("SWITCH_TITLEDB").get("TITLE")
== "Scheduled Switch TitleDB update"
)
+ assert heartbeat.get("SCHEDULER").get("LAUNCHBOX_METADATA").get("ENABLED")
+ assert (
+ heartbeat.get("SCHEDULER").get("LAUNCHBOX_METADATA").get("CRON") == "0 5 * * *"
+ )
+ assert (
+ heartbeat.get("SCHEDULER").get("LAUNCHBOX_METADATA").get("TITLE")
+ == "Scheduled LaunchBox metadata update"
+ )
assert heartbeat.get("FRONTEND").get("UPLOAD_TIMEOUT") == 20
diff --git a/backend/handler/filesystem/roms_handler.py b/backend/handler/filesystem/roms_handler.py
index be3f6a527..943004fea 100644
--- a/backend/handler/filesystem/roms_handler.py
+++ b/backend/handler/filesystem/roms_handler.py
@@ -8,7 +8,7 @@
import zipfile
from collections.abc import Callable, Iterator
from pathlib import Path
-from typing import Any, Final, Literal, TypedDict
+from typing import IO, Any, Final, Literal, TypedDict
import magic
import py7zr
@@ -83,22 +83,23 @@ def is_compressed_file(file_path: str) -> bool:
)
-def read_basic_file(file_path: Path) -> Iterator[bytes]:
+def read_basic_file(file_path: os.PathLike[str]) -> Iterator[bytes]:
with open(file_path, "rb") as f:
while chunk := f.read(FILE_READ_CHUNK_SIZE):
yield chunk
-def read_zip_file(file_path: Path) -> Iterator[bytes]:
+def read_zip_file(file: str | os.PathLike[str] | IO[bytes]) -> Iterator[bytes]:
try:
- with zipfile.ZipFile(file_path, "r") as z:
+ with zipfile.ZipFile(file, "r") as z:
for file in z.namelist():
with z.open(file, "r") as f:
while chunk := f.read(FILE_READ_CHUNK_SIZE):
yield chunk
except zipfile.BadZipFile:
- for chunk in read_basic_file(file_path):
- yield chunk
+ if isinstance(file, Path):
+ for chunk in read_basic_file(file):
+ yield chunk
def read_tar_file(
diff --git a/backend/scheduler.py b/backend/scheduler.py
index 2105c65ac..3b10241a1 100644
--- a/backend/scheduler.py
+++ b/backend/scheduler.py
@@ -3,6 +3,7 @@
from logger.logger import log
from tasks.scan_library import scan_library_task
from tasks.tasks import tasks_scheduler
+from tasks.update_launchbox_metadata import update_launchbox_metadata_task
from tasks.update_switch_titledb import update_switch_titledb_task
from utils import get_version
@@ -15,6 +16,7 @@
# Initialize the tasks
scan_library_task.init()
update_switch_titledb_task.init()
+ update_launchbox_metadata_task.init()
log.info("Starting scheduler")
diff --git a/backend/tasks/fixtures/launchbox/files.xml b/backend/tasks/fixtures/launchbox/files.xml
new file mode 100644
index 000000000..084152dc6
--- /dev/null
+++ b/backend/tasks/fixtures/launchbox/files.xml
@@ -0,0 +1,8 @@
+
+
+
+ 3DO Interactive Multiplayer
+ 20th Century Video Almanac (USA)
+ 20th Century Video Almanac (USA)
+
+
diff --git a/backend/tasks/fixtures/launchbox/mame.xml b/backend/tasks/fixtures/launchbox/mame.xml
new file mode 100644
index 000000000..896d25c59
--- /dev/null
+++ b/backend/tasks/fixtures/launchbox/mame.xml
@@ -0,0 +1,41 @@
+
+
+
+ wrlok_l3
+ Warlok
+ preliminary
+
+ Williams
+ 1982
+ true
+ (L-3)
+ North America
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ Electromechanical / Pinball
+ Pinball
+ English
+ pinball/s7.cpp
+
+
+ Generic Keypad
+ Keyboard
+ wrlok_l3
+ true
+
+
+ wrlok_l3
+ Warlok (L-3)
+ Williams Classics
+
+
diff --git a/backend/tasks/fixtures/launchbox/metadata.xml b/backend/tasks/fixtures/launchbox/metadata.xml
new file mode 100644
index 000000000..118f399b4
--- /dev/null
+++ b/backend/tasks/fixtures/launchbox/metadata.xml
@@ -0,0 +1,260 @@
+
+
+
+ Paper Mario
+ 2000-08-11T00:00:00-07:00
+ Mario pals around in an all-new action adventure! Mario's back in his first adventure since Super Mario 64, and this time, Bowser's bent on preventing a storybook ending. When Princess Peach is kidnapped, Mario plots to rescue the seven Star Spirits and rid the Mushroom Kingdom of Koopa's cruel cohorts. As he travels from the tropical jungles of Lavalava Island to the frosty heights of Shiver Mountain, he'll meet up with seven all-new companions... and he'll need help from each one or there'll be no happily ever after.
+ 1
+ Released
+ false
+ http://www.youtube.com/watch?v=WoGJd0k_FR8
+ 141
+ 4.4416873449131513
+ https://en.wikipedia.org/wiki/Paper_Mario_(video_game)
+ Nintendo 64
+ E - Everyone
+ 403
+ Role-Playing
+ Intelligent Systems
+ Nintendo
+
+
+ Mario Story
+ 141
+ Japan
+
+
+ 纸片马力欧
+ 141
+ China
+
+
+ 纸片马里奥
+ 141
+ China
+
+
+ 141
+ 5e4587a1-8505-4fd2-ace1-87613df5f5d1.png
+ Advertisement Flyer - Front
+ China
+ 2072259539
+
+
+ 141
+ 16231fc3-bcb3-4dbc-bbe4-f0bc36b31081.jpg
+ Advertisement Flyer - Front
+ North America
+ 727411021
+
+
+ 141
+ dfb30000-001e-432b-8e95-bd8e9fff3983.jpg
+ Advertisement Flyer - Front
+ North America
+ 583876627
+
+
+ 141
+ c417920f-1edc-4af1-95a2-2f55fb777194.jpg
+ Banner
+ 959239531
+
+
+ 141
+ e76b4339-482d-43e1-8ecb-4c6e1b442daa.jpg
+ Banner
+ 1545972692
+
+
+ 141
+ c205963e-7349-4755-8dc3-d7439a32a5a3.PNG
+ Box - 3D
+ Japan
+ 3690696107
+
+
+ 141
+ 1748e1a1-a1aa-4726-b9e8-4c04da848d58.png
+ Box - 3D
+ Europe
+ 2680176444
+
+
+ 141
+ c7ae433d-5e91-4da5-bb75-24b84b68e69c.png
+ Box - 3D
+ North America
+ 3778415585
+
+
+ 141
+ 0edddd5d-4175-423d-8418-9340c9eae803.jpg
+ Box - Back
+ North America
+ 4144827731
+
+
+ 141
+ 11a54e4a-6831-4f99-bff2-dc2e9c5bf779.jpg
+ Box - Back
+ Japan
+ 1527171011
+
+
+ 141
+ 10e9028c-3c48-44d1-9272-aac002e26c86.jpg
+ Box - Back
+ Europe
+ 1577175924
+
+
+ 141
+ 346c7be3-6533-4418-80a3-55c61f2abc11.jpg
+ Box - Front
+ Europe
+ 2640187913
+
+
+ 141
+ 856a0c08-0d44-4142-8315-8d0c72a679c5.jpg
+ Box - Front
+ Japan
+ 386642606
+
+
+ 141
+ eb7eb36a-2a90-4100-aa6b-1c9736e38ae6.png
+ Box - Front
+ North America
+ 390632758
+
+
+ 141
+ 310332a4-41d3-4606-bdb1-fedd21f0ea79.jpg
+ Box - Front
+ Australia
+ 936530111
+
+
+ 141
+ d5755b52-d75c-405d-98e7-20e54526bed3.jpg
+ Box - Spine
+ North America
+ 3913618231
+
+
+ 141
+ 8a47e006-a565-4906-8420-02e6de9a33ad.jpg
+ Box - Spine
+ Japan
+ 1761128152
+
+
+ 141
+ 90115c9c-48b9-4b79-b8ea-2033519d9923.png
+ Cart - 3D
+ North America
+ 3602298032
+
+
+ 141
+ 87d84d99-db78-4cbc-8450-fda0ffc3ce80.png
+ Cart - Front
+ North America
+ 22235904
+
+
+ 141
+ 1e0a96eb-a772-4003-9777-290ac0ea8356.png
+ Clear Logo
+ World
+ 2486841913
+
+
+ 141
+ 7fa59a62-99a9-4daa-a8b4-cf9681a73b55.png
+ Clear Logo
+ China
+ 1965202817
+
+
+ 141
+ bde294fa-b2d8-46dd-83da-4dc72623d922.png
+ Clear Logo
+ Japan
+ 1297661257
+
+
+ 141
+ fb6214d1-b0ba-4193-97d6-82128de00930.png
+ Clear Logo
+ Japan
+ 2974004483
+
+
+ 141
+ 87f88c4e-689d-4eef-958c-e65f957f4243.jpg
+ Fanart - Background
+ 3930859962
+
+
+ 141
+ fa81cb25-99d9-4de8-9526-929c1b791f4e.jpg
+ Fanart - Background
+ 397167211
+
+
+ 141
+ 70b9b270-361c-4134-8590-19760800245f.png
+ Fanart - Box - Front
+ 1787674306
+
+
+ 141
+ 7cf2a436-9ba6-42a4-9fe7-b805d6da2ccf.png
+ Screenshot - Game Title
+ China
+ 765939706
+
+
+ 141
+ af53b508-1585-4522-bb31-de6eb5c7f6e1.png
+ Screenshot - Game Title
+ China
+ 698006263
+
+
+ 141
+ 363e6708-2ec8-495a-be53-492e31d3d5f8.png
+ Screenshot - Game Title
+ Japan
+ 4083516316
+
+
+ 141
+ 561f42f1-9f37-4787-82b8-dd8156399c29.png
+ Screenshot - Game Title
+ North America
+ 2631830900
+
+
+ 141
+ 3f33ae92-6a49-4428-81b6-10ba85f4e027.png
+ Screenshot - Gameplay
+ 991180833
+
+
+ 141
+ 927204b2-64ee-4379-99a2-2a114c26f64b.png
+ Screenshot - Gameplay
+ 3132977303
+
+
+ 141
+ 15f6304e-85be-4863-a65d-9a81f25e7707.png
+ Screenshot - Gameplay
+ China
+ 1122730830
+
+
diff --git a/backend/tasks/fixtures/launchbox/platforms.xml b/backend/tasks/fixtures/launchbox/platforms.xml
new file mode 100644
index 000000000..4db7fafa1
--- /dev/null
+++ b/backend/tasks/fixtures/launchbox/platforms.xml
@@ -0,0 +1,20 @@
+
+
+
+ 3DO Interactive Multiplayer
+ true
+ 1993-10-04T00:00:00-07:00
+ The 3DO Company
+ Panasonic Corporation, Sanyo Electric Co., Ltd., GoldStar
+ 32-bit RISC ARM60 @ 12.5 MHz
+ 2 MB main RAM and 1 MB video RAM
+ 3DO "CLIO" @ 25 MHz, 3DO "MADAM" @ 25 MHz
+ 16-bit Stereo, Dolby Surround
+ 320x240 and 640x480
+ CD-ROM
+ 1 (up to 8 if daisy-chained)
+ The 3DO Interactive Multiplayer (often called simply 3DO) is a video game console originally produced by Panasonic in 1993. Further renditions of the hardware were released in 1994 by Sanyo and Goldstar. The consoles were manufactured according to specifications created by The 3DO Company, and were originally designed by Dave Needle and RJ Mical of New Technology Group. The system was conceived by entrepreneur and Electronic Arts founder Trip Hawkins.
+ Consoles
+ false
+
+
diff --git a/backend/tasks/update_launchbox_metadata.py b/backend/tasks/update_launchbox_metadata.py
new file mode 100644
index 000000000..eea076c96
--- /dev/null
+++ b/backend/tasks/update_launchbox_metadata.py
@@ -0,0 +1,157 @@
+import json
+import zipfile
+from io import BytesIO
+from itertools import batched
+from typing import Final
+
+from config import (
+ ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
+ SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
+)
+from defusedxml import ElementTree as ET
+from handler.redis_handler import async_cache
+from logger.logger import log
+from tasks.tasks import RemoteFilePullTask
+from utils.context import initialize_context
+
+LAUNCHBOX_PLATFORMS_KEY: Final = "romm:launchbox_platforms"
+LAUNCHBOX_METADATA_DATABASE_ID_KEY: Final = "romm:launchbox_metadata_database_id"
+LAUNCHBOX_METADATA_NAME_KEY: Final = "romm:launchbox_metadata_name"
+LAUNCHBOX_METADATA_IMAGE_KEY: Final = "romm:launchbox_metadata_image"
+LAUNCHBOX_MAME_KEY: Final = "romm:launchbox_mame"
+
+
+class UpdateLaunchboxMetadataTask(RemoteFilePullTask):
+ def __init__(self):
+ super().__init__(
+ func="tasks.update_launchbox_metadata.update_launchbox_metadata_task.run",
+ description="launchbox metadata update",
+ enabled=ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
+ cron_string=SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
+ url="https://gamesdb.launchbox-app.com/Metadata.zip",
+ )
+
+ @initialize_context()
+ async def run(self, force: bool = False) -> None:
+ content = await super().run(force)
+ if content is None:
+ return
+
+ try:
+ zip_file_bytes = BytesIO(content)
+ with zipfile.ZipFile(zip_file_bytes) as z:
+ for file in z.namelist():
+ if file == "Platforms.xml":
+ platform_dict = {}
+ with z.open(file, "r") as f:
+ for platform in ET.parse(f).getroot().findall("Platform"):
+ name_elem = platform.find("Name")
+ assert name_elem is not None
+ platform_dict[name_elem.text] = {
+ child.tag: child.text for child in platform
+ }
+
+ async with async_cache.pipeline() as pipe:
+ for platform_batch in batched(
+ platform_dict.items(), 2000
+ ):
+ await pipe.hset(
+ LAUNCHBOX_PLATFORMS_KEY,
+ mapping={
+ k: json.dumps(v)
+ for k, v in dict(platform_batch).items()
+ },
+ )
+
+ elif file == "Metadata.xml":
+ metadata_by_id_dict: dict[str, object] = {}
+ metadata_by_name_dict: dict[str, object] = {}
+ metadata_images_by_id_dict: dict[str, list[object]] = {}
+
+ with z.open(file, "r") as f:
+ root = ET.parse(f).getroot()
+ for metadata in root.findall("Game"):
+ id_elem = metadata.find("DatabaseID")
+ assert id_elem is not None
+ metadata_by_id_dict[str(id_elem.text)] = {
+ child.tag: child.text for child in metadata
+ }
+
+ name_elem = metadata.find("Name")
+ assert name_elem is not None
+ metadata_by_name_dict[str(name_elem.text)] = {
+ child.tag: child.text for child in metadata
+ }
+
+ for image in root.findall("GameImage"):
+ id_elem = image.find("DatabaseID")
+ assert id_elem is not None
+
+ if id_elem.text not in metadata_images_by_id_dict:
+ metadata_images_by_id_dict[str(id_elem.text)] = []
+
+ metadata_images_by_id_dict[str(id_elem.text)].append(
+ {child.tag: child.text for child in image}
+ )
+
+ async with async_cache.pipeline() as pipe:
+ for mbid_batch in batched(
+ metadata_by_id_dict.items(), 2000
+ ):
+ await pipe.hset(
+ LAUNCHBOX_METADATA_DATABASE_ID_KEY,
+ mapping={
+ k: json.dumps(v)
+ for k, v in dict(mbid_batch).items()
+ },
+ )
+
+ for mbn_batch in batched(
+ metadata_by_name_dict.items(), 2000
+ ):
+ await pipe.hset(
+ LAUNCHBOX_METADATA_NAME_KEY,
+ mapping={
+ k: json.dumps(v)
+ for k, v in dict(mbn_batch).items()
+ },
+ )
+
+ for data_batch in batched(
+ metadata_images_by_id_dict.items(), 2000
+ ):
+
+ await pipe.hset(
+ LAUNCHBOX_METADATA_IMAGE_KEY,
+ mapping={
+ k: json.dumps(v)
+ for k, v in dict(data_batch).items()
+ },
+ )
+ elif file == "Mame.xml":
+ mame_dict = {}
+ with z.open(file, "r") as f:
+ for mame in ET.parse(f).getroot().findall("MameFile"):
+ filename_elem = mame.find("FileName")
+ assert filename_elem is not None
+ mame_dict[filename_elem.text] = {
+ child.tag: child.text for child in mame
+ }
+
+ async with async_cache.pipeline() as pipe:
+ for mame_batch in batched(mame_dict.items(), 2000):
+ await pipe.hset(
+ LAUNCHBOX_MAME_KEY,
+ mapping={
+ k: json.dumps(v)
+ for k, v in dict(mame_batch).items()
+ },
+ )
+ except zipfile.BadZipFile:
+ log.error("Bad zip file in launchbox metadata update")
+ return
+
+ log.info("Scheduled launchbox metadata update completed!")
+
+
+update_launchbox_metadata_task = UpdateLaunchboxMetadataTask()
diff --git a/backend/tools/xml_diagnostics.py b/backend/tools/xml_diagnostics.py
new file mode 100644
index 000000000..57b366588
--- /dev/null
+++ b/backend/tools/xml_diagnostics.py
@@ -0,0 +1,84 @@
+import sys
+from xml.sax.handler import ContentHandler # nosec
+
+from defusedxml import ElementTree as ET
+from defusedxml.sax import make_parser
+
+
+class DiagnosticHandler(ContentHandler):
+ def __init__(self):
+ super().__init__()
+ self.line_number = 0
+ self.column_number = 0
+
+ def setDocumentLocator(self, locator):
+ self.locator = locator
+
+ def characters(self, content):
+ # Check for invalid XML characters
+ for char in content:
+ if ord(char) >= 0xFFFE or (ord(char) <= 0x1F and char not in "\n\r\t"):
+ print(
+ f"Found invalid character '0x{ord(char):04x}' at line {self.locator.getLineNumber()}, column {self.locator.getColumnNumber()}"
+ )
+
+
+def diagnose_xml(filename):
+ print(f"Analyzing {filename}...")
+
+ # First, try to read the file in chunks to find encoding issues
+ try:
+ with open(filename, "rb") as f:
+ chunk_size = 8192
+ chunk_number = 0
+ while True:
+ chunk = f.read(chunk_size)
+ if not chunk:
+ break
+ try:
+ chunk.decode("utf-8")
+ except UnicodeDecodeError as e:
+ byte_pos = chunk_number * chunk_size + e.start
+ print(f"Found invalid UTF-8 sequence at byte position {byte_pos}")
+ print(f"Problematic bytes: {chunk[e.start:e.end].decode('utf-8')}")
+ chunk_number += 1
+ except Exception as e:
+ print(f"Error reading file: {e}")
+ return
+
+ # Then try SAX parsing for detailed error reporting
+ parser = make_parser()
+ handler = DiagnosticHandler()
+ parser.setContentHandler(handler)
+
+ try:
+ parser.parse(filename)
+ except Exception as e:
+ print(f"SAX parsing error: {e}")
+
+ # Finally try ElementTree parsing
+ try:
+ ET.parse(filename)
+ except ET.ParseError as e:
+ print(f"ElementTree parsing error: {e}")
+
+ # Try to get context around the error
+ try:
+ with open(filename, "r", encoding="utf-8") as f:
+ lines = f.readlines()
+ line_num = e.position[0]
+ start = max(0, line_num - 2)
+ end = min(len(lines), line_num + 3)
+ print("\nContext around error:")
+ for i in range(start, end):
+ prefix = "-> " if i + 1 == line_num else " "
+ print(f"{prefix}{i+1}: {lines[i].rstrip()}")
+ except Exception as context_error:
+ print(f"Could not get context: {context_error}")
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("Usage: python script.py ")
+ sys.exit(1)
+ diagnose_xml(sys.argv[1])
diff --git a/env.template b/env.template
index 31fe2683d..22308c85b 100644
--- a/env.template
+++ b/env.template
@@ -59,6 +59,8 @@ ENABLE_SCHEDULED_RESCAN=true
SCHEDULED_RESCAN_CRON=0 3 * * *
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB=true
SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON=0 4 * * *
+ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA=true
+SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON= 0 5 * * *
# In-browser emulation
DISABLE_EMULATOR_JS=false
diff --git a/frontend/src/__generated__/models/SchedulerDict.ts b/frontend/src/__generated__/models/SchedulerDict.ts
index a4a41c7f9..edee58592 100644
--- a/frontend/src/__generated__/models/SchedulerDict.ts
+++ b/frontend/src/__generated__/models/SchedulerDict.ts
@@ -6,5 +6,6 @@ import type { TaskDict } from './TaskDict';
export type SchedulerDict = {
RESCAN: TaskDict;
SWITCH_TITLEDB: TaskDict;
+ LAUNCHBOX_METADATA: TaskDict;
};
diff --git a/frontend/src/components/Settings/Administration/Tasks.vue b/frontend/src/components/Settings/Administration/Tasks.vue
index f1dec2d62..baf3b49f5 100644
--- a/frontend/src/components/Settings/Administration/Tasks.vue
+++ b/frontend/src/components/Settings/Administration/Tasks.vue
@@ -44,6 +44,19 @@ const tasks = computed(() => [
: "mdi-clock-remove-outline",
enabled: heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.ENABLED,
},
+ {
+ title: heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.TITLE,
+ description:
+ heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.MESSAGE +
+ " " +
+ convertCronExperssion(
+ heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.CRON,
+ ),
+ icon: heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.ENABLED
+ ? "mdi-clock-check-outline"
+ : "mdi-clock-remove-outline",
+ enabled: heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.ENABLED,
+ },
]);
// Functions
diff --git a/poetry.lock b/poetry.lock
index 0f0298c4d..0a31ece40 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -169,10 +169,6 @@ files = [
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
- {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"},
- {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"},
- {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"},
- {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"},
{file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
{file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
{file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
@@ -185,14 +181,8 @@ files = [
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
- {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"},
- {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"},
- {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"},
- {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"},
{file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
{file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
- {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"},
- {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"},
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
{file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
@@ -203,24 +193,8 @@ files = [
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
- {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"},
- {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"},
- {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"},
- {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"},
{file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
{file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
- {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"},
- {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"},
- {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"},
- {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"},
- {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"},
- {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"},
- {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"},
- {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"},
- {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"},
- {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"},
- {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"},
- {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"},
{file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
@@ -230,10 +204,6 @@ files = [
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
- {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"},
- {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"},
- {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"},
- {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"},
{file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
{file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
{file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
@@ -245,10 +215,6 @@ files = [
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
- {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"},
- {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"},
- {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"},
- {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"},
{file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
{file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
{file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
@@ -261,10 +227,6 @@ files = [
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
- {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"},
- {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"},
- {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"},
- {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"},
{file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
{file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
{file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
@@ -277,10 +239,6 @@ files = [
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
- {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"},
- {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"},
- {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"},
- {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"},
{file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
{file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
@@ -578,6 +536,18 @@ files = [
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+groups = ["main"]
+files = [
+ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
+
[[package]]
name = "emoji"
version = "2.10.1"
@@ -2722,7 +2692,7 @@ description = "Python for Window Extensions"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "platform_python_implementation != \"PyPy\" and sys_platform == \"win32\" and extra == \"dev\""
+markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\" and extra == \"dev\""
files = [
{file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"},
{file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"},
@@ -4092,4 +4062,4 @@ test = ["fakeredis", "pytest", "pytest-asyncio", "pytest-env", "pytest-mock", "p
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
-content-hash = "cd26d8392654433fd53cea5277d3deb5afd847cf2c48f9bebb8fa7bf5f4be66c"
+content-hash = "158126e169d3541036a3910a64447a36cf5094ff51215cc5a58f2f375f3e2db4"
diff --git a/pyproject.toml b/pyproject.toml
index 03e84c88d..7154d8970 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,6 +51,7 @@ dependencies = [
"websockets == 12.0",
"yarl ~= 1.14",
"zipfile-deflate64 ~= 0.2",
+ "defusedxml ~= 0.7.1",
]
[project.optional-dependencies]
diff --git a/pytest.ini b/pytest.ini
index 56e64e9b2..00a75dd5e 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -11,9 +11,7 @@ env =
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE=true
ENABLE_SCHEDULED_RESCAN=true
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB=true
- RESCAN_ON_FILESYSTEM_CHANGE_DELAY=5
- SCHEDULED_RESCAN_CRON=0 3 * * *
- SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON=0 4 * * *
+ ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA=true
UPLOAD_TIMEOUT=20
LOGLEVEL=DEBUG
OIDC_ENABLED=false