refactor: standardize config_update event for UI
This commit is contained in:
parent
8a10627d6a
commit
ea1fc59e88
13 changed files with 192 additions and 159 deletions
18
API.md
18
API.md
|
|
@ -72,6 +72,7 @@ This document describes the available endpoints and their usage. All endpoints r
|
|||
- [POST /api/system/pause](#post-apisystempause)
|
||||
- [POST /api/system/resume](#post-apisystemresume)
|
||||
- [POST /api/system/shutdown](#post-apisystemshutdown)
|
||||
- [POST /api/system/check-updates](#post-apisystemcheck-updates)
|
||||
- [GET /api/dev/loop](#get-apidevloop)
|
||||
- [GET /api/dev/pip](#get-apidevpip)
|
||||
- [GET /api/docs/{file}](#get-apidocsfile)
|
||||
|
|
@ -102,9 +103,7 @@ This document describes the available endpoints and their usage. All endpoints r
|
|||
- [`cli_post` (Client → Server)](#cli_post-client--server)
|
||||
- [`cli_output` (Server → Client)](#cli_output-server--client)
|
||||
- [`cli_close` (Server → Client)](#cli_close-server--client)
|
||||
- [Configuration Events](#configuration-events)
|
||||
- [`presets_update` (Server → Client)](#presets_update-server--client)
|
||||
- [`dlfields_update` (Server → Client)](#dlfields_update-server--client)
|
||||
- [Error Responses](#error-responses)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -2087,19 +2086,6 @@ Emitted when CLI command execution completes.
|
|||
**Data Fields**:
|
||||
- `exitcode`: Command exit code (0 = success, non-zero = error)
|
||||
|
||||
### Configuration Events
|
||||
|
||||
#### `presets_update` (Server → Client)
|
||||
Emitted when download presets are updated or created.
|
||||
|
||||
**Data**: Array of preset objects
|
||||
|
||||
#### `dlfields_update` (Server → Client)
|
||||
Emitted when download fields configuration is updated.
|
||||
|
||||
**Data**: Array of field objects
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ from pydantic import ValidationError
|
|||
|
||||
from app.features.conditions.schemas import Condition, ConditionList, ConditionPatch
|
||||
from app.features.conditions.service import Conditions
|
||||
from app.features.core.schemas import Pagination
|
||||
from app.features.core.schemas import CEAction, CEFeature, ConfigEvent, Pagination
|
||||
from app.features.core.utils import build_pagination, format_validation_errors, normalize_pagination
|
||||
from app.library.cache import Cache
|
||||
from app.library.config import Config
|
||||
from app.library.encoder import Encoder
|
||||
from app.library.Events import EventBus, Events
|
||||
from app.library.router import route
|
||||
|
||||
LOG: logging.Logger = logging.getLogger(__name__)
|
||||
|
|
@ -136,7 +137,7 @@ async def conditions_test(request: Request, encoder: Encoder, cache: Cache, conf
|
|||
|
||||
|
||||
@route("POST", "api/conditions/", name="condition_add")
|
||||
async def conditions_add(request: Request, encoder: Encoder) -> Response:
|
||||
async def conditions_add(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||
"""
|
||||
Add Condition.
|
||||
|
||||
|
|
@ -166,15 +167,14 @@ async def conditions_add(request: Request, encoder: Encoder) -> Response:
|
|||
)
|
||||
|
||||
try:
|
||||
data = _serialize(await Conditions.get_instance().save(item=item.model_dump()))
|
||||
saved = _serialize(await Conditions.get_instance().save(item=item.model_dump()))
|
||||
except ValueError as exc:
|
||||
return web.json_response({"error": str(exc)}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
return web.json_response(
|
||||
data=data,
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=encoder.encode,
|
||||
notify.emit(
|
||||
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.CREATE, data=saved)
|
||||
)
|
||||
return web.json_response(data=saved, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
|
||||
|
||||
@route("GET", "api/conditions/{id}", name="condition_get")
|
||||
|
|
@ -200,13 +200,14 @@ async def conditions_get(request: Request, encoder: Encoder) -> Response:
|
|||
|
||||
|
||||
@route("DELETE", "api/conditions/{id}", name="condition_delete")
|
||||
async def conditions_delete(request: Request, encoder: Encoder) -> Response:
|
||||
async def conditions_delete(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||
"""
|
||||
Delete Condition.
|
||||
|
||||
Args:
|
||||
request (Request): The request object.
|
||||
encoder (Encoder): The encoder instance.
|
||||
notify (EventBus): The event bus instance.
|
||||
|
||||
Returns:
|
||||
Response: The response object.
|
||||
|
|
@ -216,23 +217,24 @@ async def conditions_delete(request: Request, encoder: Encoder) -> Response:
|
|||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
try:
|
||||
return web.json_response(
|
||||
data=_serialize(await Conditions.get_instance()._repo.delete(id)),
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=encoder.encode,
|
||||
deleted = _serialize(await Conditions.get_instance()._repo.delete(id))
|
||||
notify.emit(
|
||||
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.DELETE, data=deleted)
|
||||
)
|
||||
return web.json_response(data=deleted, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
except KeyError as exc:
|
||||
return web.json_response({"error": str(exc)}, status=web.HTTPNotFound.status_code)
|
||||
|
||||
|
||||
@route("PATCH", "api/conditions/{id}", name="condition_patch")
|
||||
async def conditions_patch(request: Request, encoder: Encoder) -> Response:
|
||||
async def conditions_patch(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||
"""
|
||||
Patch Condition.
|
||||
|
||||
Args:
|
||||
request (Request): The request object.
|
||||
encoder (Encoder): The encoder instance.
|
||||
notify (EventBus): The event bus instance.
|
||||
|
||||
Returns:
|
||||
Response: The response object.
|
||||
|
|
@ -268,23 +270,22 @@ async def conditions_patch(request: Request, encoder: Encoder) -> Response:
|
|||
status=web.HTTPConflict.status_code,
|
||||
)
|
||||
|
||||
dct = validated.model_dump(exclude_unset=True)
|
||||
|
||||
return web.json_response(
|
||||
data=_serialize(await service._repo.update(model.id, dct)),
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=encoder.encode,
|
||||
updated = _serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||
notify.emit(
|
||||
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.UPDATE, data=updated)
|
||||
)
|
||||
return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
|
||||
|
||||
@route("PUT", "api/conditions/{id}", name="condition_update")
|
||||
async def conditions_update(request: Request, encoder: Encoder) -> Response:
|
||||
async def conditions_update(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||
"""
|
||||
Update Condition.
|
||||
|
||||
Args:
|
||||
request (Request): The request object.
|
||||
encoder (Encoder): The encoder instance.
|
||||
notify (EventBus): The event bus instance.
|
||||
|
||||
Returns:
|
||||
Response: The response object.
|
||||
|
|
@ -320,8 +321,8 @@ async def conditions_update(request: Request, encoder: Encoder) -> Response:
|
|||
status=web.HTTPConflict.status_code,
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
data=_serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True))),
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=encoder.encode,
|
||||
updated = _serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||
notify.emit(
|
||||
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.UPDATE, data=updated)
|
||||
)
|
||||
return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Pagination(BaseModel):
|
||||
|
|
@ -10,3 +13,29 @@ class Pagination(BaseModel):
|
|||
total_pages: int
|
||||
has_next: bool
|
||||
has_prev: bool
|
||||
|
||||
|
||||
class CEFeature(str, Enum):
|
||||
PRESETS = "presets"
|
||||
DL_FIELDS = "dl_fields"
|
||||
CONDITIONS = "conditions"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class CEAction(str, Enum):
|
||||
CREATE = "create"
|
||||
UPDATE = "update"
|
||||
DELETE = "delete"
|
||||
REPLACE = "replace"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class ConfigEvent(BaseModel):
|
||||
feature: CEFeature
|
||||
action: CEAction
|
||||
data: dict[str, Any] | list[dict[str, Any]]
|
||||
extras: dict[str, Any] = Field(default_factory=dict)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from aiohttp import web
|
|||
from aiohttp.web import Request, Response
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.features.core.schemas import Pagination
|
||||
from app.features.core.schemas import CEAction, CEFeature, ConfigEvent, Pagination
|
||||
from app.features.core.utils import build_pagination, format_validation_errors, normalize_pagination
|
||||
from app.features.dl_fields.schemas import DLField, DLFieldList, DLFieldPatch
|
||||
from app.features.dl_fields.service import DLFields
|
||||
|
|
@ -62,7 +62,7 @@ async def dl_fields_add(request: Request, encoder: Encoder, notify: EventBus) ->
|
|||
except ValueError as exc:
|
||||
return web.json_response({"error": str(exc)}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
notify.emit(Events.DLFIELDS_UPDATE, data=[saved])
|
||||
notify.emit(Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.CREATE, data=saved))
|
||||
|
||||
return web.json_response(data=saved, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
|
||||
|
|
@ -79,13 +79,17 @@ async def dl_fields_get(request: Request, encoder: Encoder) -> Response:
|
|||
|
||||
|
||||
@route("DELETE", "api/dl_fields/{id}", "dl_fields_delete")
|
||||
async def dl_fields_delete(request: Request, encoder: Encoder) -> Response:
|
||||
async def dl_fields_delete(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||
if not (identifier := request.match_info.get("id")):
|
||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
try:
|
||||
deleted = _serialize(await DLFields.get_instance()._repo.delete(identifier))
|
||||
notify.emit(
|
||||
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.DELETE, data=deleted)
|
||||
)
|
||||
return web.json_response(
|
||||
data=_serialize(await DLFields.get_instance()._repo.delete(identifier)),
|
||||
data=deleted,
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=encoder.encode,
|
||||
)
|
||||
|
|
@ -94,7 +98,7 @@ async def dl_fields_delete(request: Request, encoder: Encoder) -> Response:
|
|||
|
||||
|
||||
@route("PATCH", "api/dl_fields/{id}", "dl_fields_patch")
|
||||
async def dl_fields_patch(request: Request, encoder: Encoder) -> Response:
|
||||
async def dl_fields_patch(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||
if not (identifier := request.match_info.get("id")):
|
||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
|
|
@ -123,17 +127,19 @@ async def dl_fields_patch(request: Request, encoder: Encoder) -> Response:
|
|||
status=web.HTTPConflict.status_code,
|
||||
)
|
||||
|
||||
dct = validated.model_dump(exclude_unset=True)
|
||||
|
||||
updated = _serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||
notify.emit(
|
||||
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.UPDATE, data=updated)
|
||||
)
|
||||
return web.json_response(
|
||||
data=_serialize(await DLFields.get_instance()._repo.update(model.id, dct)),
|
||||
data=updated,
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=encoder.encode,
|
||||
)
|
||||
|
||||
|
||||
@route("PUT", "api/dl_fields/{id}", "dl_fields_update")
|
||||
async def dl_fields_update(request: Request, encoder: Encoder) -> Response:
|
||||
async def dl_fields_update(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||
if not (identifier := request.match_info.get("id")):
|
||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
|
|
@ -162,8 +168,9 @@ async def dl_fields_update(request: Request, encoder: Encoder) -> Response:
|
|||
status=web.HTTPConflict.status_code,
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
data=_serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True))),
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=encoder.encode,
|
||||
updated = _serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||
notify.emit(
|
||||
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.UPDATE, data=updated)
|
||||
)
|
||||
|
||||
return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class Events:
|
|||
CONNECTED: str = "connected"
|
||||
|
||||
CONFIGURATION: str = "configuration"
|
||||
CONFIG_UPDATE: str = "config_update"
|
||||
ACTIVE_QUEUE: str = "active_queue"
|
||||
|
||||
LOG_INFO: str = "log_info"
|
||||
|
|
@ -58,17 +59,8 @@ class Events:
|
|||
TASK_FINISHED: str = "task_finished"
|
||||
TASK_ERROR: str = "task_error"
|
||||
|
||||
PRESETS_ADD: str = "presets_add"
|
||||
PRESETS_UPDATE: str = "presets_update"
|
||||
|
||||
DLFIELDS_ADD: str = "dlfields_add"
|
||||
DLFIELDS_UPDATE: str = "dlfields_update"
|
||||
|
||||
SCHEDULE_ADD: str = "schedule_add"
|
||||
|
||||
CONDITIONS_ADD: str = "conditions_add"
|
||||
CONDITIONS_UPDATE: str = "conditions_update"
|
||||
|
||||
SUBSCRIBED: str = "subscribed"
|
||||
UNSUBSCRIBED: str = "unsubscribed"
|
||||
|
||||
|
|
@ -94,6 +86,7 @@ class Events:
|
|||
"""
|
||||
return [
|
||||
Events.CONFIGURATION,
|
||||
Events.CONFIG_UPDATE,
|
||||
Events.CONNECTED,
|
||||
Events.ACTIVE_QUEUE,
|
||||
Events.LOG_INFO,
|
||||
|
|
@ -110,8 +103,6 @@ class Events:
|
|||
Events.RESUMED,
|
||||
Events.CLI_CLOSE,
|
||||
Events.CLI_OUTPUT,
|
||||
Events.PRESETS_UPDATE,
|
||||
Events.DLFIELDS_UPDATE,
|
||||
]
|
||||
|
||||
def only_debug() -> list:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from typing import Any
|
|||
from aiohttp import web
|
||||
|
||||
from .config import Config
|
||||
from .Events import EventBus, Events
|
||||
from .Singleton import Singleton
|
||||
from .Utils import arg_converter, create_cookies_file, init_class
|
||||
|
||||
|
|
@ -214,12 +213,6 @@ class Presets(metaclass=Singleton):
|
|||
LOG.error(f"Default preset '{self._config.default_preset}' not found, using 'default' preset.")
|
||||
self._config.default_preset = "default"
|
||||
|
||||
async def event_handler(_, __):
|
||||
msg = "Not implemented"
|
||||
raise Exception(msg)
|
||||
|
||||
EventBus.get_instance().subscribe(Events.PRESETS_ADD, event_handler, f"{__class__.__name__}.add")
|
||||
|
||||
def get_all(self) -> list[Preset]:
|
||||
"""Return the items."""
|
||||
return sorted(self._default + self._items, key=lambda x: x.priority, reverse=True)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import uuid
|
|||
from aiohttp import web
|
||||
from aiohttp.web import Request, Response
|
||||
|
||||
from app.features.core.schemas import CEAction, CEFeature, ConfigEvent
|
||||
from app.library.encoder import Encoder
|
||||
from app.library.Events import EventBus, Events
|
||||
from app.library.Presets import Preset, Presets
|
||||
|
|
@ -96,5 +97,12 @@ async def presets_add(request: Request, encoder: Encoder, notify: EventBus) -> R
|
|||
status=web.HTTPInternalServerError.status_code,
|
||||
)
|
||||
|
||||
notify.emit(Events.PRESETS_UPDATE, data=presets)
|
||||
notify.emit(
|
||||
Events.CONFIG_UPDATE,
|
||||
data=ConfigEvent(
|
||||
feature=CEFeature.PRESETS,
|
||||
action=CEAction.REPLACE,
|
||||
data=[preset.serialize() for preset in presets],
|
||||
),
|
||||
)
|
||||
return web.json_response(data=presets, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class TestEvents:
|
|||
|
||||
# Connection events
|
||||
assert Events.CONNECTED == "connected"
|
||||
assert Events.CONFIG_UPDATE == "config_update"
|
||||
|
||||
# Log events
|
||||
assert Events.LOG_INFO == "log_info"
|
||||
|
|
@ -48,6 +49,7 @@ class TestEvents:
|
|||
"started",
|
||||
"shutdown",
|
||||
"connected",
|
||||
"config_update",
|
||||
"log_info",
|
||||
"log_warning",
|
||||
"log_error",
|
||||
|
|
@ -85,6 +87,7 @@ class TestEvents:
|
|||
# Check some expected frontend events
|
||||
expected_frontend = [
|
||||
Events.CONNECTED,
|
||||
Events.CONFIG_UPDATE,
|
||||
Events.LOG_INFO,
|
||||
Events.LOG_WARNING,
|
||||
Events.LOG_ERROR,
|
||||
|
|
|
|||
|
|
@ -103,11 +103,9 @@ class TestPresets:
|
|||
Presets._reset_singleton()
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_singleton(self, mock_eventbus, mock_config):
|
||||
def test_presets_singleton(self, mock_config):
|
||||
"""Test that Presets follows singleton pattern."""
|
||||
mock_config.get_instance.return_value = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
instance1 = Presets.get_instance()
|
||||
instance2 = Presets.get_instance()
|
||||
|
|
@ -116,13 +114,10 @@ class TestPresets:
|
|||
assert isinstance(instance1, Presets)
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_initialization(self, mock_eventbus, mock_config):
|
||||
def test_presets_initialization(self, mock_config):
|
||||
"""Test Presets initialization with default presets."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus_instance = Mock()
|
||||
mock_eventbus.get_instance.return_value = mock_eventbus_instance
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -138,12 +133,10 @@ class TestPresets:
|
|||
assert preset.default is True
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_load_empty_file(self, mock_eventbus, mock_config):
|
||||
def test_presets_load_empty_file(self, mock_config):
|
||||
"""Test loading presets from empty or non-existent file."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -157,13 +150,11 @@ class TestPresets:
|
|||
assert len(presets._default) > 0 # Should still have default presets
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
@patch("app.library.Presets.arg_converter")
|
||||
def test_presets_load_valid_file(self, mock_arg_converter, mock_eventbus, mock_config):
|
||||
def test_presets_load_valid_file(self, mock_arg_converter, mock_config):
|
||||
"""Test loading presets from valid JSON file."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
||||
|
||||
test_presets = [
|
||||
|
|
@ -184,13 +175,11 @@ class TestPresets:
|
|||
assert presets._items[0].name == "test_preset_1"
|
||||
assert presets._items[1].name == "test_preset_2"
|
||||
|
||||
@patch("app.library.Presets.EventBus")
|
||||
@patch("app.library.Presets.Config")
|
||||
def test_presets_load_invalid_json(self, mock_eventbus, mock_config):
|
||||
def test_presets_load_invalid_json(self, mock_config):
|
||||
"""Test loading presets from invalid JSON file."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -204,13 +193,11 @@ class TestPresets:
|
|||
assert len(presets._items) == 0
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
@patch("app.library.Presets.arg_converter")
|
||||
def test_presets_load_with_format_migration(self, mock_arg_converter, mock_eventbus, mock_config):
|
||||
def test_presets_load_with_format_migration(self, mock_arg_converter, mock_config):
|
||||
"""Test loading presets with old format that needs migration."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
||||
|
||||
# Old format preset with 'format' field instead of 'cli'
|
||||
|
|
@ -233,12 +220,10 @@ class TestPresets:
|
|||
assert loaded_preset.id is not None
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_validate_valid_preset(self, mock_eventbus, mock_config):
|
||||
def test_presets_validate_valid_preset(self, mock_config):
|
||||
"""Test validating a valid preset."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -250,12 +235,10 @@ class TestPresets:
|
|||
assert result is True
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_validate_invalid_preset(self, mock_eventbus, mock_config):
|
||||
def test_presets_validate_invalid_preset(self, mock_config):
|
||||
"""Test validating invalid presets."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -274,13 +257,11 @@ class TestPresets:
|
|||
presets.validate("invalid_type")
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
@patch("app.library.Presets.arg_converter")
|
||||
def test_presets_save(self, mock_arg_converter, mock_eventbus, mock_config):
|
||||
def test_presets_save(self, mock_arg_converter, mock_config):
|
||||
"""Test saving presets to file."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
||||
|
||||
test_presets = [
|
||||
|
|
@ -303,12 +284,10 @@ class TestPresets:
|
|||
assert saved_data[0]["name"] == "test1"
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_get_all(self, mock_eventbus, mock_config):
|
||||
def test_presets_get_all(self, mock_config):
|
||||
"""Test getting all presets (default + custom)."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -325,12 +304,10 @@ class TestPresets:
|
|||
assert any(p.default is True for p in all_presets)
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_get_by_id(self, mock_eventbus, mock_config):
|
||||
def test_presets_get_by_id(self, mock_config):
|
||||
"""Test getting preset by ID."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -347,12 +324,10 @@ class TestPresets:
|
|||
assert found_preset.name == "test_preset"
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_get_by_name(self, mock_eventbus, mock_config):
|
||||
def test_presets_get_by_name(self, mock_config):
|
||||
"""Test getting preset by name."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -368,12 +343,10 @@ class TestPresets:
|
|||
assert found_preset.cli == "--test"
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_get_nonexistent(self, mock_eventbus, mock_config):
|
||||
def test_presets_get_nonexistent(self, mock_config):
|
||||
"""Test getting non-existent preset returns None."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -384,12 +357,10 @@ class TestPresets:
|
|||
assert found_preset is None
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_get_empty_parameter(self, mock_eventbus, mock_config):
|
||||
def test_presets_get_empty_parameter(self, mock_config):
|
||||
"""Test getting preset with empty string returns None."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -399,12 +370,10 @@ class TestPresets:
|
|||
assert presets.get(None) is None
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_has_method(self, mock_eventbus, mock_config):
|
||||
def test_presets_has_method(self, mock_config):
|
||||
"""Test has method for checking preset existence."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -418,12 +387,10 @@ class TestPresets:
|
|||
assert presets.has("nonexistent") is False
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_clear(self, mock_eventbus, mock_config):
|
||||
def test_presets_clear(self, mock_config):
|
||||
"""Test clearing all custom presets."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -441,12 +408,10 @@ class TestPresets:
|
|||
assert len(presets._items) == 0
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_file_permissions(self, mock_eventbus, mock_config):
|
||||
def test_presets_file_permissions(self, mock_config):
|
||||
"""Test that presets file gets correct permissions."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -483,12 +448,10 @@ class TestPresets:
|
|||
assert isinstance(preset, Preset)
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_attach_method(self, mock_eventbus, mock_config):
|
||||
def test_presets_attach_method(self, mock_config):
|
||||
"""Test the attach method for aiohttp integration."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_app = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
@ -501,12 +464,10 @@ class TestPresets:
|
|||
mock_get.assert_called_once_with("default")
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_presets_attach_default_preset_not_found(self, mock_eventbus, mock_config):
|
||||
def test_presets_attach_default_preset_not_found(self, mock_config):
|
||||
"""Test attach method when default preset is not found."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="nonexistent")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_app = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
@ -520,12 +481,10 @@ class TestPresets:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
async def test_presets_on_shutdown(self, mock_eventbus, mock_config):
|
||||
async def test_presets_on_shutdown(self, mock_config):
|
||||
"""Test the on_shutdown method."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_app = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
@ -536,14 +495,12 @@ class TestPresets:
|
|||
await presets.on_shutdown(mock_app)
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
@patch("app.library.Presets.LOG")
|
||||
@patch("app.library.Presets.arg_converter")
|
||||
def test_presets_load_invalid_preset_in_file(self, mock_arg_converter, mock_log, mock_eventbus, mock_config):
|
||||
def test_presets_load_invalid_preset_in_file(self, mock_arg_converter, mock_log, mock_config):
|
||||
"""Test loading file with some invalid presets."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
||||
|
||||
# Mix of valid and invalid presets
|
||||
|
|
@ -570,13 +527,11 @@ class TestPresets:
|
|||
mock_log.error.assert_called()
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
@patch("app.library.Presets.LOG")
|
||||
def test_presets_save_with_invalid_preset(self, mock_log, mock_eventbus, mock_config):
|
||||
def test_presets_save_with_invalid_preset(self, mock_log, mock_config):
|
||||
"""Test saving presets with some invalid ones."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
# Create preset with invalid CLI that will fail validation
|
||||
valid_preset = Preset(name="valid", cli="--valid-option")
|
||||
|
|
@ -596,12 +551,10 @@ class TestPresets:
|
|||
mock_log.error.assert_called()
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_preset_priority_validation_integer(self, mock_eventbus, mock_config):
|
||||
def test_preset_priority_validation_integer(self, mock_config):
|
||||
"""Test that priority must be an integer."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -614,12 +567,10 @@ class TestPresets:
|
|||
presets.validate(invalid_preset)
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_preset_priority_validation_negative(self, mock_eventbus, mock_config):
|
||||
def test_preset_priority_validation_negative(self, mock_config):
|
||||
"""Test that priority must be >= 0."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -632,12 +583,10 @@ class TestPresets:
|
|||
presets.validate(invalid_preset)
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_preset_priority_sorting(self, mock_eventbus, mock_config):
|
||||
def test_preset_priority_sorting(self, mock_config):
|
||||
"""Test that presets are sorted by priority in descending order."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
@ -661,19 +610,16 @@ class TestPresets:
|
|||
assert non_default[2].priority == 1
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_preset_priority_default_zero(self, mock_eventbus, mock_config):
|
||||
def test_preset_priority_default_zero(self, mock_config):
|
||||
"""Test that priority defaults to 0 if not specified."""
|
||||
preset = Preset(name="test")
|
||||
assert preset.priority == 0
|
||||
|
||||
@patch("app.library.Presets.Config")
|
||||
@patch("app.library.Presets.EventBus")
|
||||
def test_preset_priority_migration(self, mock_eventbus, mock_config):
|
||||
def test_preset_priority_migration(self, mock_config):
|
||||
"""Test that old presets without priority get it added on load."""
|
||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
presets_file = Path(temp_dir) / "presets.json"
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@
|
|||
<li><code>source_name:task_name</code> - items added by the specified task.</li>
|
||||
</ul>
|
||||
</Message>
|
||||
<Message v-else-if="socket.isConnected" class="is-primary" title="No items" icon="fas fa-exclamation-triangle"
|
||||
<Message v-else class="is-primary" title="No items" icon="fas fa-exclamation-triangle"
|
||||
:new-style="true">
|
||||
<p>Download history is empty.</p>
|
||||
</Message>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { useStorage } from '@vueuse/core'
|
||||
import type { ConfigState } from '~/types/config';
|
||||
import type { DLField } from '~/types/dl_fields';
|
||||
import type { Preset } from '~/types/presets';
|
||||
import type { ConfigFeature, ConfigUpdateAction } from '~/types/sockets';
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const state = reactive<ConfigState>({
|
||||
|
|
@ -65,6 +68,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||
}
|
||||
return (state as any)[key] ?? defaultValue
|
||||
}
|
||||
const isLoaded = () => state.is_loaded
|
||||
|
||||
const update = add
|
||||
|
||||
|
|
@ -84,16 +88,62 @@ export const useConfigStore = defineStore('config', () => {
|
|||
state.is_loaded = true
|
||||
}
|
||||
|
||||
const isLoaded = () => state.is_loaded
|
||||
const patch = (feature: ConfigFeature, action: ConfigUpdateAction, data: unknown): void => {
|
||||
const supportedFeatures: ConfigFeature[] = ['dl_fields', 'presets']
|
||||
|
||||
if (!supportedFeatures.includes(feature)) {
|
||||
return
|
||||
}
|
||||
|
||||
if ('presets' === feature) {
|
||||
if ('replace' === action) {
|
||||
state.presets = data as Array<Preset>
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ('dl_fields' === feature) {
|
||||
const item = data as DLField
|
||||
const current = get(feature, []) as Array<DLField>
|
||||
|
||||
if ('create' === action) {
|
||||
current.push(item)
|
||||
return
|
||||
}
|
||||
|
||||
if ('delete' === action) {
|
||||
const index = current.findIndex(i => i.id === item.id)
|
||||
if (-1 !== index) {
|
||||
current.splice(index, 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ('update' === action) {
|
||||
const target = current.find(i => i.id === item.id)
|
||||
if (target) {
|
||||
Object.assign(target, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ('replace' === action) {
|
||||
state.dl_fields = data as Array<DLField>
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
...toRefs(state), add, get, update, getAll, setAll, isLoaded,
|
||||
...toRefs(state), add, get, update, getAll, setAll, isLoaded, patch
|
||||
} as { [K in keyof ConfigState]: Ref<ConfigState[K]> } & {
|
||||
add: typeof add
|
||||
get: typeof get
|
||||
update: typeof update
|
||||
getAll: typeof getAll
|
||||
setAll: typeof setAll
|
||||
patch: typeof patch
|
||||
isLoaded: typeof isLoaded
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ref, readonly } from 'vue'
|
|||
import { defineStore } from 'pinia'
|
||||
import type { ConfigState } from "~/types/config";
|
||||
import type { StoreItem } from "~/types/store";
|
||||
import type { ConfigUpdatePayload } from "~/types/sockets";
|
||||
|
||||
export type connectionStatus = 'connected' | 'disconnected' | 'connecting';
|
||||
|
||||
|
|
@ -277,8 +278,13 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
toast.warning('Download queue paused.', { timeout: 10000 });
|
||||
}, true);
|
||||
|
||||
on('presets_update', (data: string) => config.update('presets', JSON.parse(data).data || []));
|
||||
on('dlfields_update', (data: string) => config.update('dl_fields', JSON.parse(data).data || []));
|
||||
on('config_update', (stream: string) => {
|
||||
const json = JSON.parse(stream) as { data: ConfigUpdatePayload }
|
||||
if (!json?.data) {
|
||||
return
|
||||
}
|
||||
config.patch(json.data.feature, json.data.action, json.data.data)
|
||||
})
|
||||
|
||||
setupVisibilityListener();
|
||||
}
|
||||
|
|
|
|||
13
ui/app/types/sockets.d.ts
vendored
13
ui/app/types/sockets.d.ts
vendored
|
|
@ -6,3 +6,16 @@ export type Event = {
|
|||
message: string | null
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
type ConfigUpdateAction = 'create' | 'update' | 'delete' | 'replace'
|
||||
|
||||
type ConfigFeature = 'presets' | 'dl_fields' | 'conditions'
|
||||
|
||||
type ConfigUpdatePayload<T = unknown> = {
|
||||
feature: ConfigFeature
|
||||
action: ConfigUpdateAction
|
||||
data: T | Array<T>
|
||||
meta?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type { ConfigUpdateAction, ConfigFeature, ConfigUpdatePayload }
|
||||
|
|
|
|||
Loading…
Reference in a new issue