mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 23:42:07 +01:00
feat: Added clean resources task + revamped the whole task system
This commit is contained in:
@@ -2,19 +2,11 @@ from config import (
|
||||
DISABLE_EMULATOR_JS,
|
||||
DISABLE_RUFFLE_RS,
|
||||
DISABLE_USERPASS_LOGIN,
|
||||
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
|
||||
ENABLE_SCHEDULED_RESCAN,
|
||||
ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
|
||||
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB,
|
||||
HASHEOUS_API_ENABLED,
|
||||
LAUNCHBOX_API_ENABLED,
|
||||
OIDC_ENABLED,
|
||||
OIDC_PROVIDER,
|
||||
PLAYMATCH_API_ENABLED,
|
||||
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
|
||||
SCHEDULED_RESCAN_CRON,
|
||||
SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
|
||||
SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON,
|
||||
TGDB_API_ENABLED,
|
||||
UPLOAD_TIMEOUT,
|
||||
)
|
||||
@@ -68,31 +60,6 @@ async def heartbeat() -> HeartbeatResponse:
|
||||
"FILESYSTEM": {
|
||||
"FS_PLATFORMS": await fs_platform_handler.get_platforms(),
|
||||
},
|
||||
"WATCHER": {
|
||||
"ENABLED": ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
|
||||
"TITLE": "Rescan on filesystem change",
|
||||
"MESSAGE": f"Runs a scan when a change is detected in the library path, with a {RESCAN_ON_FILESYSTEM_CHANGE_DELAY} minute delay",
|
||||
},
|
||||
"SCHEDULER": {
|
||||
"RESCAN": {
|
||||
"ENABLED": ENABLE_SCHEDULED_RESCAN,
|
||||
"CRON": SCHEDULED_RESCAN_CRON,
|
||||
"TITLE": "Scheduled rescan",
|
||||
"MESSAGE": "Rescans the entire library",
|
||||
},
|
||||
"SWITCH_TITLEDB": {
|
||||
"ENABLED": ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB, # noqa
|
||||
"CRON": SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON,
|
||||
"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 store",
|
||||
},
|
||||
},
|
||||
"EMULATION": {
|
||||
"DISABLE_EMULATOR_JS": DISABLE_EMULATOR_JS,
|
||||
"DISABLE_RUFFLE_RS": DISABLE_RUFFLE_RS,
|
||||
|
||||
@@ -6,22 +6,6 @@ class SystemDict(TypedDict):
|
||||
SHOW_SETUP_WIZARD: bool
|
||||
|
||||
|
||||
class WatcherDict(TypedDict):
|
||||
ENABLED: bool
|
||||
TITLE: str
|
||||
MESSAGE: str
|
||||
|
||||
|
||||
class TaskDict(WatcherDict):
|
||||
CRON: str
|
||||
|
||||
|
||||
class SchedulerDict(TypedDict):
|
||||
RESCAN: TaskDict
|
||||
SWITCH_TITLEDB: TaskDict
|
||||
LAUNCHBOX_METADATA: TaskDict
|
||||
|
||||
|
||||
class MetadataSourcesDict(TypedDict):
|
||||
ANY_SOURCE_ENABLED: bool
|
||||
IGDB_API_ENABLED: bool
|
||||
@@ -56,8 +40,6 @@ class OIDCDict(TypedDict):
|
||||
|
||||
class HeartbeatResponse(TypedDict):
|
||||
SYSTEM: SystemDict
|
||||
WATCHER: WatcherDict
|
||||
SCHEDULER: SchedulerDict
|
||||
METADATA_SOURCES: MetadataSourcesDict
|
||||
FILESYSTEM: FilesystemDict
|
||||
EMULATION: EmulationDict
|
||||
|
||||
14
backend/endpoints/responses/tasks.py
Normal file
14
backend/endpoints/responses/tasks.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from typing import Dict, List, TypedDict
|
||||
|
||||
|
||||
class TaskInfoDict(TypedDict):
|
||||
name: str
|
||||
manual_run: bool
|
||||
title: str
|
||||
description: str
|
||||
enabled: bool
|
||||
cron_string: str
|
||||
|
||||
|
||||
# Use a more flexible type for grouped tasks
|
||||
GroupedTasksDict = Dict[str, List[TaskInfoDict]]
|
||||
@@ -1,46 +1,189 @@
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from config import (
|
||||
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
|
||||
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
|
||||
)
|
||||
from decorators.auth import protected_route
|
||||
from endpoints.responses import MessageResponse
|
||||
from fastapi import Request
|
||||
from endpoints.responses.tasks import GroupedTasksDict, TaskInfoDict
|
||||
from fastapi import HTTPException, 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 logger.logger import log
|
||||
from utils.router import APIRouter
|
||||
|
||||
TASK_TYPES = ["scheduled", "manual"]
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/tasks",
|
||||
tags=["tasks"],
|
||||
)
|
||||
|
||||
|
||||
def _get_available_tasks() -> Dict[str, Any]:
|
||||
"""Automatically discover all available tasks by scanning the tasks directory."""
|
||||
tasks = {}
|
||||
|
||||
for task_type in TASK_TYPES:
|
||||
tasks_dir = Path(__file__).parent.parent / "tasks" / task_type
|
||||
|
||||
for task_file in tasks_dir.glob("*.py"):
|
||||
module_name = f"tasks.{task_type}.{task_file.stem}"
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
# Look for task instances (variables ending with _task)
|
||||
for attr_name in dir(module):
|
||||
if attr_name.endswith("_task") and not attr_name.startswith("_"):
|
||||
task_instance = getattr(module, attr_name)
|
||||
# Verify it has a run method
|
||||
if hasattr(task_instance, "run") and callable(
|
||||
task_instance.run
|
||||
):
|
||||
# Use the task name without the _task suffix as the key
|
||||
task_key = attr_name.replace("_task", "")
|
||||
tasks[task_key] = task_instance
|
||||
|
||||
except ImportError as e:
|
||||
log.error(f"Warning: Could not import task module {module_name}: {e}")
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
@protected_route(router.get, "", [Scope.TASKS_RUN])
|
||||
async def list_tasks(request: Request) -> GroupedTasksDict:
|
||||
"""List all available tasks grouped by task type.
|
||||
|
||||
Args:
|
||||
request (Request): FastAPI Request object
|
||||
Returns:
|
||||
Dictionary with tasks grouped by their type (scheduled, manual, watcher)
|
||||
"""
|
||||
tasks = _get_available_tasks()
|
||||
|
||||
# Initialize the grouped tasks dictionary
|
||||
grouped_tasks: GroupedTasksDict = {}
|
||||
|
||||
# Group tasks by type
|
||||
for task_type in TASK_TYPES:
|
||||
task_list: list[TaskInfoDict] = []
|
||||
tasks_dir = Path(__file__).parent.parent / "tasks" / task_type
|
||||
|
||||
for name, instance in tasks.items():
|
||||
# Check if this task belongs to the current type by checking if it exists in the type directory
|
||||
task_file_path = tasks_dir / f"{name}.py"
|
||||
if task_file_path.exists():
|
||||
manual_run = getattr(instance, "manual_run", False)
|
||||
title = getattr(instance, "title", name.replace("_", " ").title())
|
||||
description = getattr(
|
||||
instance, "description", "No description available"
|
||||
)
|
||||
enabled = getattr(instance, "enabled", False)
|
||||
task_info: TaskInfoDict = {
|
||||
"name": name,
|
||||
"manual_run": manual_run,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"enabled": enabled,
|
||||
"cron_string": getattr(instance, "cron_string", ""),
|
||||
}
|
||||
task_list.append(task_info)
|
||||
|
||||
if task_list:
|
||||
grouped_tasks[task_type] = task_list
|
||||
|
||||
# Add the adhoc watcher task
|
||||
watcher_task: TaskInfoDict = {
|
||||
"name": "filesystem_watcher",
|
||||
"manual_run": False,
|
||||
"title": "Rescan on filesystem change",
|
||||
"description": f"Runs a scan when a change is detected in the library path, with a {RESCAN_ON_FILESYSTEM_CHANGE_DELAY} minute delay",
|
||||
"enabled": ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
|
||||
"cron_string": "",
|
||||
}
|
||||
grouped_tasks["watcher"] = [watcher_task]
|
||||
|
||||
return grouped_tasks
|
||||
|
||||
|
||||
@protected_route(router.post, "/run", [Scope.TASKS_RUN])
|
||||
async def run_tasks(request: Request) -> MessageResponse:
|
||||
"""Run all tasks endpoint
|
||||
async def run_all_tasks(request: Request) -> MessageResponse:
|
||||
"""Run all runnable tasks endpoint
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
request (Request): FastAPI Request object
|
||||
Returns:
|
||||
RunTasksResponse: Standard message response
|
||||
MessageResponse: Standard message response
|
||||
"""
|
||||
tasks = _get_available_tasks()
|
||||
|
||||
await update_switch_titledb_task.run()
|
||||
await update_launchbox_metadata_task.run()
|
||||
return {"msg": "All tasks ran successfully!"}
|
||||
if not tasks:
|
||||
return {"msg": "No tasks available to run"}
|
||||
|
||||
|
||||
@protected_route(router.post, "/{task}/run", [Scope.TASKS_RUN])
|
||||
async def run_task(request: Request, task: str) -> MessageResponse:
|
||||
"""Run single tasks endpoint
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
Returns:
|
||||
RunTasksResponse: Standard message response
|
||||
"""
|
||||
|
||||
tasks = {
|
||||
"switch_titledb": update_switch_titledb_task,
|
||||
"launchbox_metadata": update_launchbox_metadata_task,
|
||||
# Filter only runnable tasks
|
||||
runnable_tasks = {
|
||||
name: instance
|
||||
for name, instance in tasks.items()
|
||||
if getattr(instance, "manual_run", False)
|
||||
}
|
||||
|
||||
await tasks[task].run()
|
||||
return {"msg": f"Task {task} run successfully!"}
|
||||
if not runnable_tasks:
|
||||
return {"msg": "No runnable tasks available to run"}
|
||||
|
||||
failed_tasks = []
|
||||
successful_tasks = []
|
||||
|
||||
for task_name, task_instance in runnable_tasks.items():
|
||||
try:
|
||||
await task_instance.run()
|
||||
successful_tasks.append(task_name)
|
||||
except Exception as e:
|
||||
failed_tasks.append(f"{task_name}: {str(e)}")
|
||||
|
||||
if failed_tasks:
|
||||
return {
|
||||
"msg": f"Some tasks failed. Successful: {', '.join(successful_tasks)}. Failed: {', '.join(failed_tasks)}"
|
||||
}
|
||||
|
||||
return {
|
||||
"msg": f"All {len(successful_tasks)} triggerable tasks ran successfully: {', '.join(successful_tasks)}"
|
||||
}
|
||||
|
||||
|
||||
@protected_route(router.post, "/run/{task_name}", [Scope.TASKS_RUN])
|
||||
async def run_single_task(request: Request, task_name: str) -> MessageResponse:
|
||||
"""Run a single task endpoint.
|
||||
|
||||
Args:
|
||||
request (Request): FastAPI Request object
|
||||
task_name (str): Name of the task to run
|
||||
Returns:
|
||||
MessageResponse: Standard message response
|
||||
"""
|
||||
tasks = _get_available_tasks()
|
||||
|
||||
if task_name not in tasks:
|
||||
available_tasks = list(tasks.keys())
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Task '{task_name}' not found. Available tasks: {', '.join(available_tasks)}",
|
||||
)
|
||||
|
||||
task_instance = tasks[task_name]
|
||||
|
||||
# Check if task is triggerable (manual_run = True)
|
||||
if not getattr(task_instance, "manual_run", False):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Task '{task_name}' is not triggerable manually.",
|
||||
)
|
||||
|
||||
try:
|
||||
await task_instance.run()
|
||||
return {"msg": f"Task '{task_name}' ran successfully!"}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Task '{task_name}' failed: {str(e)}"
|
||||
) from e
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Final, NotRequired, TypedDict
|
||||
|
||||
from handler.redis_handler import async_cache, sync_cache
|
||||
from logger.logger import log
|
||||
from tasks.update_switch_titledb import (
|
||||
from tasks.scheduled.update_switch_titledb import (
|
||||
SWITCH_PRODUCT_ID_KEY,
|
||||
SWITCH_TITLEDB_INDEX_KEY,
|
||||
update_switch_titledb_task,
|
||||
|
||||
@@ -6,7 +6,7 @@ import pydash
|
||||
from config import LAUNCHBOX_API_ENABLED, str_to_bool
|
||||
from handler.redis_handler import async_cache
|
||||
from logger.logger import log
|
||||
from tasks.update_launchbox_metadata import ( # LAUNCHBOX_MAME_KEY,
|
||||
from tasks.scheduled.update_launchbox_metadata import ( # LAUNCHBOX_MAME_KEY,
|
||||
LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY,
|
||||
LAUNCHBOX_METADATA_DATABASE_ID_KEY,
|
||||
LAUNCHBOX_METADATA_IMAGE_KEY,
|
||||
|
||||
@@ -6,10 +6,10 @@ from config import (
|
||||
SENTRY_DSN,
|
||||
)
|
||||
from logger.logger import log
|
||||
from tasks.scan_library import scan_library_task
|
||||
from tasks.scheduled.scan_library import scan_library_task
|
||||
from tasks.scheduled.update_launchbox_metadata import update_launchbox_metadata_task
|
||||
from tasks.scheduled.update_switch_titledb import update_switch_titledb_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
|
||||
|
||||
sentry_sdk.init(dsn=SENTRY_DSN, release=f"romm@{get_version()}")
|
||||
|
||||
82
backend/tasks/manual/cleanup_orphaned_resources.py
Normal file
82
backend/tasks/manual/cleanup_orphaned_resources.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from config import RESOURCES_BASE_PATH
|
||||
from handler.database import db_platform_handler, db_rom_handler
|
||||
from logger.logger import log
|
||||
from utils.context import initialize_context
|
||||
|
||||
|
||||
class CleanupOrphanedResourcesTask:
|
||||
def __init__(self):
|
||||
self.manual_run = True
|
||||
self.title = "Cleanup orphaned resources"
|
||||
self.description = "cleanup orphaned resources"
|
||||
self.enabled = True
|
||||
|
||||
@initialize_context()
|
||||
async def run(self) -> None:
|
||||
"""Clean up orphaned resources."""
|
||||
log.info(f"Starting {self.title} task...")
|
||||
|
||||
removed_count = 0
|
||||
|
||||
roms_path = os.path.join(RESOURCES_BASE_PATH, "roms")
|
||||
if not os.path.exists(RESOURCES_BASE_PATH) or not os.path.exists(roms_path):
|
||||
log.info("Resources path does not exist, skipping cleanup")
|
||||
return
|
||||
|
||||
existing_platforms = {
|
||||
str(platform.id) for platform in db_platform_handler.get_platforms()
|
||||
}
|
||||
log.debug(f"Found {len(existing_platforms)} platforms in database")
|
||||
|
||||
for platform_dir in os.listdir(roms_path):
|
||||
platform_path = os.path.join(roms_path, platform_dir)
|
||||
if not os.path.isdir(platform_path):
|
||||
continue
|
||||
|
||||
# Check if platform exists in database
|
||||
if platform_dir not in existing_platforms:
|
||||
try:
|
||||
# Remove entire platform directory if platform doesn't exist
|
||||
shutil.rmtree(platform_path)
|
||||
removed_count += 1
|
||||
log.info(f"Removed orphaned platform directory: {platform_dir}")
|
||||
except Exception as e:
|
||||
log.error(
|
||||
f"Failed to remove platform directory {platform_dir}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
platform_id = int(platform_dir)
|
||||
existing_roms = {
|
||||
str(rom.id)
|
||||
for rom in db_rom_handler.get_roms_scalar(platform_id=platform_id)
|
||||
}
|
||||
log.debug(f"Found {len(existing_roms)} ROMs for platform {platform_id}")
|
||||
|
||||
for rom_dir in os.listdir(platform_path):
|
||||
rom_path = os.path.join(platform_path, rom_dir)
|
||||
if not os.path.isdir(rom_path):
|
||||
continue
|
||||
|
||||
# Check if ROM exists in database
|
||||
if rom_dir not in existing_roms:
|
||||
try:
|
||||
# Remove ROM directory if ROM doesn't exist
|
||||
shutil.rmtree(rom_path)
|
||||
removed_count += 1
|
||||
log.info(
|
||||
f"Removed orphaned ROM directory: {platform_dir}/{rom_dir}"
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(
|
||||
f"Failed to remove ROM directory {platform_dir}/{rom_dir}: {e}"
|
||||
)
|
||||
|
||||
log.info(f"Removed {removed_count} orphaned resource directories")
|
||||
log.info("Cleanup of orphaned resources completed!")
|
||||
|
||||
|
||||
cleanup_orphaned_resources_task = CleanupOrphanedResourcesTask()
|
||||
@@ -19,10 +19,12 @@ class ScanLibraryTask(PeriodicTask):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
func="tasks.scan_library.scan_library_task.run",
|
||||
description="library scan",
|
||||
description="Rescans the entire library",
|
||||
enabled=ENABLE_SCHEDULED_RESCAN,
|
||||
cron_string=SCHEDULED_RESCAN_CRON,
|
||||
)
|
||||
self.manual_run = False
|
||||
self.title = "Scheduled rescan"
|
||||
|
||||
async def run(self):
|
||||
if not ENABLE_SCHEDULED_RESCAN:
|
||||
@@ -27,11 +27,13 @@ class UpdateLaunchboxMetadataTask(RemoteFilePullTask):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
func="tasks.update_launchbox_metadata.update_launchbox_metadata_task.run",
|
||||
description="launchbox metadata update",
|
||||
description="Updates the LaunchBox metadata store",
|
||||
enabled=ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
|
||||
cron_string=SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
|
||||
url="https://gamesdb.launchbox-app.com/Metadata.zip",
|
||||
)
|
||||
self.manual_run = True
|
||||
self.title = "Scheduled LaunchBox metadata update"
|
||||
|
||||
@initialize_context()
|
||||
async def run(self, force: bool = False) -> None:
|
||||
@@ -19,11 +19,13 @@ class UpdateSwitchTitleDBTask(RemoteFilePullTask):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
func="tasks.update_switch_titledb.update_switch_titledb_task.run",
|
||||
description="switch titledb update",
|
||||
description="Updates the Nintendo Switch TitleDB file",
|
||||
enabled=ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB,
|
||||
cron_string=SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON,
|
||||
url="https://raw.githubusercontent.com/blawar/titledb/master/US.en.json",
|
||||
)
|
||||
self.manual_run = True
|
||||
self.title = "Scheduled Switch TitleDB update"
|
||||
|
||||
@initialize_context()
|
||||
async def run(self, force: bool = False) -> None:
|
||||
@@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from handler.scan_handler import MetadataSource, ScanType
|
||||
from tasks.scan_library import ScanLibraryTask, scan_library_task
|
||||
from tasks.scheduled.scan_library import ScanLibraryTask, scan_library_task
|
||||
|
||||
|
||||
class TestScanLibraryTask:
|
||||
|
||||
@@ -3,8 +3,7 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
from tasks.tasks import RemoteFilePullTask
|
||||
from tasks.update_launchbox_metadata import (
|
||||
from tasks.scheduled.update_launchbox_metadata import (
|
||||
LAUNCHBOX_FILES_KEY,
|
||||
LAUNCHBOX_MAME_KEY,
|
||||
LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY,
|
||||
@@ -15,6 +14,7 @@ from tasks.update_launchbox_metadata import (
|
||||
UpdateLaunchboxMetadataTask,
|
||||
update_launchbox_metadata_task,
|
||||
)
|
||||
from tasks.tasks import RemoteFilePullTask
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -2,13 +2,13 @@ import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from tasks.tasks import RemoteFilePullTask
|
||||
from tasks.update_switch_titledb import (
|
||||
from tasks.scheduled.update_switch_titledb import (
|
||||
SWITCH_PRODUCT_ID_KEY,
|
||||
SWITCH_TITLEDB_INDEX_KEY,
|
||||
UpdateSwitchTitleDBTask,
|
||||
update_switch_titledb_task,
|
||||
)
|
||||
from tasks.tasks import RemoteFilePullTask
|
||||
|
||||
|
||||
class TestUpdateSwitchTitleDBTask:
|
||||
|
||||
4
frontend/src/__generated__/index.ts
generated
4
frontend/src/__generated__/index.ts
generated
@@ -54,7 +54,6 @@ export type { RomSSMetadata } from './models/RomSSMetadata';
|
||||
export type { RomUserSchema } from './models/RomUserSchema';
|
||||
export type { RomUserStatus } from './models/RomUserStatus';
|
||||
export type { SaveSchema } from './models/SaveSchema';
|
||||
export type { SchedulerDict } from './models/SchedulerDict';
|
||||
export type { ScreenshotSchema } from './models/ScreenshotSchema';
|
||||
export type { SearchCoverSchema } from './models/SearchCoverSchema';
|
||||
export type { SearchRomSchema } from './models/SearchRomSchema';
|
||||
@@ -63,7 +62,7 @@ export type { SimpleRomSchema } from './models/SimpleRomSchema';
|
||||
export type { StateSchema } from './models/StateSchema';
|
||||
export type { StatsReturn } from './models/StatsReturn';
|
||||
export type { SystemDict } from './models/SystemDict';
|
||||
export type { TaskDict } from './models/TaskDict';
|
||||
export type { TaskInfoDict } from './models/TaskInfoDict';
|
||||
export type { TinfoilFeedFileSchema } from './models/TinfoilFeedFileSchema';
|
||||
export type { TinfoilFeedSchema } from './models/TinfoilFeedSchema';
|
||||
export type { TinfoilFeedTitleDBSchema } from './models/TinfoilFeedTitleDBSchema';
|
||||
@@ -73,7 +72,6 @@ export type { UserNotesSchema } from './models/UserNotesSchema';
|
||||
export type { UserSchema } from './models/UserSchema';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
export type { VirtualCollectionSchema } from './models/VirtualCollectionSchema';
|
||||
export type { WatcherDict } from './models/WatcherDict';
|
||||
export type { WebrcadeFeedCategorySchema } from './models/WebrcadeFeedCategorySchema';
|
||||
export type { WebrcadeFeedItemPropsSchema } from './models/WebrcadeFeedItemPropsSchema';
|
||||
export type { WebrcadeFeedItemSchema } from './models/WebrcadeFeedItemSchema';
|
||||
|
||||
@@ -7,13 +7,9 @@ import type { FilesystemDict } from './FilesystemDict';
|
||||
import type { FrontendDict } from './FrontendDict';
|
||||
import type { MetadataSourcesDict } from './MetadataSourcesDict';
|
||||
import type { OIDCDict } from './OIDCDict';
|
||||
import type { SchedulerDict } from './SchedulerDict';
|
||||
import type { SystemDict } from './SystemDict';
|
||||
import type { WatcherDict } from './WatcherDict';
|
||||
export type HeartbeatResponse = {
|
||||
SYSTEM: SystemDict;
|
||||
WATCHER: WatcherDict;
|
||||
SCHEDULER: SchedulerDict;
|
||||
METADATA_SOURCES: MetadataSourcesDict;
|
||||
FILESYSTEM: FilesystemDict;
|
||||
EMULATION: EmulationDict;
|
||||
|
||||
11
frontend/src/__generated__/models/SchedulerDict.ts
generated
11
frontend/src/__generated__/models/SchedulerDict.ts
generated
@@ -1,11 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { TaskDict } from './TaskDict';
|
||||
export type SchedulerDict = {
|
||||
RESCAN: TaskDict;
|
||||
SWITCH_TITLEDB: TaskDict;
|
||||
LAUNCHBOX_METADATA: TaskDict;
|
||||
};
|
||||
|
||||
11
frontend/src/__generated__/models/TaskDict.ts
generated
11
frontend/src/__generated__/models/TaskDict.ts
generated
@@ -1,11 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TaskDict = {
|
||||
ENABLED: boolean;
|
||||
TITLE: string;
|
||||
MESSAGE: string;
|
||||
CRON: string;
|
||||
};
|
||||
|
||||
13
frontend/src/__generated__/models/TaskInfoDict.ts
generated
Normal file
13
frontend/src/__generated__/models/TaskInfoDict.ts
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TaskInfoDict = {
|
||||
name: string;
|
||||
manual_run: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
cron_string: string;
|
||||
};
|
||||
|
||||
10
frontend/src/__generated__/models/WatcherDict.ts
generated
10
frontend/src/__generated__/models/WatcherDict.ts
generated
@@ -1,10 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type WatcherDict = {
|
||||
ENABLED: boolean;
|
||||
TITLE: string;
|
||||
MESSAGE: string;
|
||||
};
|
||||
|
||||
@@ -1,27 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
import { inject, computed } from "vue";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import api from "@/services/api/index";
|
||||
import type { Emitter } from "mitt";
|
||||
import storeRunningTasks from "@/stores/runningTasks";
|
||||
|
||||
// Props
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
enabled: boolean;
|
||||
enabled?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
name?: string;
|
||||
manual_run?: boolean;
|
||||
cron_string?: string;
|
||||
}>(),
|
||||
{ title: "", description: "", icon: "" },
|
||||
{
|
||||
enabled: true,
|
||||
title: "",
|
||||
description: "",
|
||||
icon: "",
|
||||
name: "",
|
||||
manual_run: false,
|
||||
cron_string: "",
|
||||
},
|
||||
);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const runningTasksStore = storeRunningTasks();
|
||||
|
||||
// Computed properties
|
||||
const isTaskRunning = computed(() =>
|
||||
props.name ? runningTasksStore.isTaskRunning(props.name) : false,
|
||||
);
|
||||
|
||||
// Functions
|
||||
function run() {
|
||||
if (!props.name) return;
|
||||
|
||||
// Add task to running tasks
|
||||
runningTasksStore.addTask(props.name);
|
||||
|
||||
api
|
||||
.post(`/tasks/run/${props.name}`)
|
||||
.then(() => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Task '${props.title}' ran successfully!`,
|
||||
icon: "mdi-check-bold",
|
||||
color: "green",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: error.response.data.detail,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
// Remove task from running tasks
|
||||
runningTasksStore.removeTask(props.name);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<v-card elevation="0" :disabled="!enabled" class="bg-background">
|
||||
<v-list-item class="pa-0">
|
||||
<v-list-item-title
|
||||
class="font-weight-bold"
|
||||
:class="{ 'text-primary': enabled }"
|
||||
>{{ title }}</v-list-item-title
|
||||
>
|
||||
<v-list-item-subtitle>{{ description }}</v-list-item-subtitle>
|
||||
<template #prepend
|
||||
><v-icon
|
||||
:class="enabled ? 'text-primary' : ''"
|
||||
:icon="icon" /></template
|
||||
></v-list-item>
|
||||
<v-card elevation="0" class="bg-background">
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-list-item :disabled="!enabled" class="pa-0">
|
||||
<template #prepend>
|
||||
<v-icon :class="enabled ? 'text-primary' : ''" :icon="icon" />
|
||||
</template>
|
||||
<v-list-item-title
|
||||
class="font-weight-bold"
|
||||
:class="{ 'text-primary': enabled }"
|
||||
>{{ title }}</v-list-item-title
|
||||
>
|
||||
<v-list-item-subtitle>{{ description }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
<v-col v-if="manual_run" cols="auto" class="d-flex align-center">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
size="small"
|
||||
class="text-primary"
|
||||
:disabled="isTaskRunning"
|
||||
:loading="isTaskRunning"
|
||||
@click="run"
|
||||
>
|
||||
<v-icon>mdi-play</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -2,102 +2,89 @@
|
||||
import Task from "@/components/Settings/Administration/TaskOption.vue";
|
||||
import RSection from "@/components/common/RSection.vue";
|
||||
import api from "@/services/api/index";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeRunningTasks from "@/stores/runningTasks";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { TaskInfoDict } from "@/__generated__/models/TaskInfoDict";
|
||||
import { convertCronExperssion } from "@/utils";
|
||||
import type { Emitter } from "mitt";
|
||||
import { computed, inject } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
// Props
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const heartbeatStore = storeHeartbeat();
|
||||
const runningTasks = storeRunningTasks();
|
||||
const tasks = computed(() => [
|
||||
{
|
||||
title: heartbeatStore.value.WATCHER.TITLE,
|
||||
description: heartbeatStore.value.WATCHER.MESSAGE,
|
||||
icon: heartbeatStore.value.WATCHER.ENABLED
|
||||
? "mdi-file-check-outline"
|
||||
: "mdi-file-remove-outline",
|
||||
enabled: heartbeatStore.value.WATCHER.ENABLED,
|
||||
},
|
||||
{
|
||||
title: heartbeatStore.value.SCHEDULER.RESCAN.TITLE,
|
||||
description:
|
||||
heartbeatStore.value.SCHEDULER.RESCAN.MESSAGE +
|
||||
" " +
|
||||
convertCronExperssion(heartbeatStore.value.SCHEDULER.RESCAN.CRON),
|
||||
icon: heartbeatStore.value.SCHEDULER.RESCAN.ENABLED
|
||||
? "mdi-clock-check-outline"
|
||||
: "mdi-clock-remove-outline",
|
||||
enabled: heartbeatStore.value.SCHEDULER.RESCAN.ENABLED,
|
||||
},
|
||||
{
|
||||
title: heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.TITLE,
|
||||
description:
|
||||
heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.MESSAGE +
|
||||
" " +
|
||||
convertCronExperssion(heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.CRON),
|
||||
icon: heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.ENABLED
|
||||
? "mdi-clock-check-outline"
|
||||
: "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,
|
||||
},
|
||||
]);
|
||||
const tasks = ref<{
|
||||
watcher?: Array<TaskInfoDict>;
|
||||
scheduled?: Array<TaskInfoDict>;
|
||||
manual?: Array<TaskInfoDict>;
|
||||
}>({});
|
||||
|
||||
const watcherTasks = computed(
|
||||
() =>
|
||||
tasks.value.watcher?.map((task) => ({
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
icon: task.enabled ? "mdi-file-check-outline" : "mdi-file-remove-outline",
|
||||
enabled: task.enabled,
|
||||
name: task.name,
|
||||
})) || [],
|
||||
);
|
||||
|
||||
const scheduledTasks = computed(
|
||||
() =>
|
||||
tasks.value.scheduled?.map((task) => ({
|
||||
title: task.title,
|
||||
description:
|
||||
task.description + " " + convertCronExperssion(task.cron_string),
|
||||
icon: task.enabled
|
||||
? "mdi-clock-check-outline"
|
||||
: "mdi-clock-remove-outline",
|
||||
enabled: task.enabled,
|
||||
name: task.name,
|
||||
manual_run: task.manual_run,
|
||||
cron_string: convertCronExperssion(task.cron_string),
|
||||
})) || [],
|
||||
);
|
||||
|
||||
// Icon mapping for manual tasks
|
||||
const getManualTaskIcon = (taskName: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
cleanup_orphaned_resources: "mdi-broom",
|
||||
};
|
||||
return iconMap[taskName] || "mdi-play";
|
||||
};
|
||||
|
||||
const manualTasks = computed(
|
||||
() =>
|
||||
tasks.value.manual?.map((task) => ({
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
icon: getManualTaskIcon(task.name),
|
||||
name: task.name,
|
||||
manual_run: task.manual_run,
|
||||
})) || [],
|
||||
);
|
||||
|
||||
// Functions
|
||||
const runAllTasks = async () => {
|
||||
runningTasks.value = true;
|
||||
const result = await api.post("/tasks/run");
|
||||
runningTasks.value = false;
|
||||
if (result.status !== 200) {
|
||||
return emitter?.emit("snackbarShow", {
|
||||
msg: "Error running tasks",
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: result.data.msg,
|
||||
icon: "mdi-check-circle",
|
||||
color: "green",
|
||||
const getAvailableTasks = async () => {
|
||||
await api.get("/tasks").then((response) => {
|
||||
tasks.value = response.data;
|
||||
});
|
||||
console.log(tasks.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getAvailableTasks();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<r-section icon="mdi-pulse" title="Tasks" class="ma-2">
|
||||
<template #toolbar-append>
|
||||
<v-btn-group class="mr-3" divided density="compact">
|
||||
<v-btn
|
||||
:disabled="runningTasks.value"
|
||||
:loading="runningTasks.value"
|
||||
prepend-icon="mdi-play"
|
||||
variant="outlined"
|
||||
class="text-primary"
|
||||
@click="runAllTasks"
|
||||
>
|
||||
Run All
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</template>
|
||||
<template #toolbar-append> </template>
|
||||
<template #content>
|
||||
<v-row no-gutters class="align-center">
|
||||
<v-col cols="12" md="6" v-for="task in tasks">
|
||||
<v-chip
|
||||
label
|
||||
variant="text"
|
||||
prepend-icon="mdi-folder-eye"
|
||||
class="ml-2 mt-1"
|
||||
>Watcher</v-chip
|
||||
>
|
||||
<v-divider class="border-opacity-25 ma-1" />
|
||||
<v-row no-gutters class="align-center py-1">
|
||||
<v-col cols="12" md="6" v-for="task in watcherTasks">
|
||||
<task
|
||||
class="ma-3"
|
||||
:enabled="task.enabled"
|
||||
@@ -107,6 +94,45 @@ const runAllTasks = async () => {
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-chip label variant="text" prepend-icon="mdi-clock" class="ml-2 mt-1"
|
||||
>Scheduled</v-chip
|
||||
>
|
||||
<v-divider class="border-opacity-25 ma-1" />
|
||||
<v-row no-gutters class="align-center py-1">
|
||||
<v-col cols="12" md="6" v-for="task in scheduledTasks">
|
||||
<task
|
||||
class="ma-3"
|
||||
:enabled="task.enabled"
|
||||
:title="task.title"
|
||||
:description="task.description"
|
||||
:icon="task.icon"
|
||||
:name="task.name"
|
||||
:manual_run="task.manual_run"
|
||||
:cron_string="task.cron_string"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row no-gutters class="align-center py-1">
|
||||
<v-chip
|
||||
label
|
||||
variant="text"
|
||||
prepend-icon="mdi-gesture-double-tap"
|
||||
class="ml-2 mt-1"
|
||||
>Manual</v-chip
|
||||
>
|
||||
<v-divider class="border-opacity-25 ma-1" />
|
||||
<v-col cols="12" md="6" v-for="task in manualTasks">
|
||||
<task
|
||||
class="ma-3"
|
||||
:title="task.title"
|
||||
:description="task.description"
|
||||
:icon="task.icon"
|
||||
:name="task.name"
|
||||
:manual_run="task.manual_run"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</r-section>
|
||||
</template>
|
||||
|
||||
@@ -168,7 +168,7 @@ onMounted(() => {
|
||||
/>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn-group divided density="compact">
|
||||
<v-btn-group divided density="compact" variant="text">
|
||||
<v-btn
|
||||
size="small"
|
||||
@click="emitter?.emit('showEditUserDialog', item)"
|
||||
|
||||
@@ -15,31 +15,6 @@ const defaultHeartbeat: Heartbeat = {
|
||||
VERSION: "0.0.0",
|
||||
SHOW_SETUP_WIZARD: false,
|
||||
},
|
||||
WATCHER: {
|
||||
ENABLED: false,
|
||||
TITLE: "",
|
||||
MESSAGE: "",
|
||||
},
|
||||
SCHEDULER: {
|
||||
RESCAN: {
|
||||
ENABLED: false,
|
||||
TITLE: "",
|
||||
MESSAGE: "",
|
||||
CRON: "",
|
||||
},
|
||||
SWITCH_TITLEDB: {
|
||||
ENABLED: false,
|
||||
TITLE: "",
|
||||
MESSAGE: "",
|
||||
CRON: "",
|
||||
},
|
||||
LAUNCHBOX_METADATA: {
|
||||
ENABLED: false,
|
||||
TITLE: "",
|
||||
MESSAGE: "",
|
||||
CRON: "",
|
||||
},
|
||||
},
|
||||
METADATA_SOURCES: {
|
||||
ANY_SOURCE_ENABLED: false,
|
||||
IGDB_API_ENABLED: false,
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export default defineStore("runningTasks", {
|
||||
state: () => ({ value: false }),
|
||||
state: () => ({
|
||||
runningTasks: [] as string[],
|
||||
}),
|
||||
|
||||
actions: {
|
||||
set(runningTasks: boolean) {
|
||||
this.value = runningTasks;
|
||||
},
|
||||
reset() {
|
||||
this.value = false;
|
||||
this.runningTasks = [];
|
||||
},
|
||||
addTask(taskEndpoint: string) {
|
||||
if (!this.runningTasks.includes(taskEndpoint)) {
|
||||
this.runningTasks.push(taskEndpoint);
|
||||
}
|
||||
},
|
||||
removeTask(taskEndpoint: string) {
|
||||
const index = this.runningTasks.indexOf(taskEndpoint);
|
||||
if (index > -1) {
|
||||
this.runningTasks.splice(index, 1);
|
||||
}
|
||||
},
|
||||
isTaskRunning(taskEndpoint: string): boolean {
|
||||
return this.runningTasks.includes(taskEndpoint);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user