355 lines
12 KiB
Python
355 lines
12 KiB
Python
import asyncio
|
|
from collections import OrderedDict
|
|
from typing import Any
|
|
|
|
from aiohttp import web
|
|
from aiohttp.web import Request, Response
|
|
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 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.log import get_logger
|
|
from app.library.router import route
|
|
from app.library.Utils import validate_url
|
|
|
|
LOG = get_logger()
|
|
|
|
|
|
def _model(model: Any) -> Condition:
|
|
return Condition.model_validate(model)
|
|
|
|
|
|
def _serialize(model: Any) -> dict:
|
|
return _model(model).model_dump()
|
|
|
|
|
|
@route("GET", "api/conditions/", name="conditions_list")
|
|
async def conditions_list(request: Request, encoder: Encoder) -> Response:
|
|
"""
|
|
Get the conditions
|
|
|
|
Args:
|
|
request (Request): The request object.
|
|
encoder (Encoder): The encoder instance.
|
|
|
|
Returns:
|
|
Response: The response object.
|
|
|
|
"""
|
|
repo = Conditions.get_instance()._repo
|
|
|
|
page, per_page = normalize_pagination(request)
|
|
items, total, current_page, total_pages = await repo.list_paginated(page, per_page)
|
|
return web.json_response(
|
|
data=ConditionList(
|
|
items=[_model(model) for model in items],
|
|
pagination=Pagination.model_validate(build_pagination(total, current_page, per_page, total_pages)),
|
|
),
|
|
status=web.HTTPOk.status_code,
|
|
dumps=encoder.encode,
|
|
)
|
|
|
|
|
|
@route("POST", "api/conditions/test/", name="condition_test")
|
|
async def conditions_test(request: Request, encoder: Encoder, cache: Cache, config: Config) -> Response:
|
|
"""
|
|
Test condition against URL.
|
|
|
|
Args:
|
|
request (Request): The request object.
|
|
encoder (Encoder): The encoder instance.
|
|
cache (Cache): The cache instance.
|
|
config (Config): The configuration instance.
|
|
|
|
Returns:
|
|
Response: The response object
|
|
|
|
"""
|
|
params = await request.json()
|
|
|
|
if not isinstance(params, dict):
|
|
return web.json_response(
|
|
{"error": "Invalid request body expecting dict."},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
|
|
if not (url := params.get("url")):
|
|
return web.json_response({"error": "url is required."}, status=web.HTTPBadRequest.status_code)
|
|
|
|
if not (cond := params.get("condition")):
|
|
return web.json_response({"error": "condition is required."}, status=web.HTTPBadRequest.status_code)
|
|
|
|
try:
|
|
await asyncio.to_thread(validate_url, url, config.allow_internal_urls)
|
|
except ValueError as e:
|
|
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
|
|
|
|
try:
|
|
preset: str = params.get("preset", config.default_preset)
|
|
key: str = cache.hash(url + str(preset))
|
|
if not cache.has(key):
|
|
from app.features.ytdlp.extractor import fetch_info
|
|
from app.features.ytdlp.ytdlp_opts import YTDLPOpts
|
|
|
|
(data, _) = await fetch_info(
|
|
config=YTDLPOpts.get_instance().preset(name=preset).get_all(),
|
|
url=url,
|
|
debug=False,
|
|
no_archive=True,
|
|
follow_redirect=True,
|
|
sanitize_info=True,
|
|
)
|
|
|
|
if not data:
|
|
return web.json_response(
|
|
data={"error": f"Failed to extract info from '{url!s}'."},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
cache.set(key=key, value=data, ttl=600)
|
|
else:
|
|
data = cache.get(key)
|
|
except Exception as e:
|
|
LOG.exception(
|
|
"Failed to extract video info for condition check '%s'.",
|
|
cond,
|
|
extra={
|
|
"route": "conditions.match",
|
|
"condition": cond,
|
|
"url": url,
|
|
"preset": preset,
|
|
"exception_type": type(e).__name__,
|
|
},
|
|
)
|
|
return web.json_response(
|
|
data={"error": f"Failed to extract video info. '{e!s}'"},
|
|
status=web.HTTPInternalServerError.status_code,
|
|
)
|
|
|
|
try:
|
|
from app.features.ytdlp.mini_filter import match_str
|
|
|
|
status: bool = match_str(cond, data)
|
|
except Exception as e:
|
|
LOG.exception(
|
|
"Failed to evaluate condition '%s'.",
|
|
cond,
|
|
extra={
|
|
"route": "conditions.match",
|
|
"condition": cond,
|
|
"url": url,
|
|
"preset": preset,
|
|
"exception_type": type(e).__name__,
|
|
},
|
|
)
|
|
return web.json_response(
|
|
data={"error": str(e)},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
|
|
return web.json_response(
|
|
data={
|
|
"status": status,
|
|
"condition": cond,
|
|
"data": OrderedDict(sorted(data.items(), key=lambda item: len(str(item[1])))),
|
|
},
|
|
status=web.HTTPOk.status_code,
|
|
dumps=encoder.encode,
|
|
)
|
|
|
|
|
|
@route("POST", "api/conditions/", name="condition_add")
|
|
async def conditions_add(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
|
"""
|
|
Add Condition.
|
|
|
|
Args:
|
|
request (Request): The request object.
|
|
encoder (Encoder): The encoder instance.
|
|
notify (EventBus): The event bus instance.
|
|
|
|
Returns:
|
|
Response: The response object
|
|
|
|
"""
|
|
data = await request.json()
|
|
|
|
if not isinstance(data, dict):
|
|
return web.json_response(
|
|
{"error": "Invalid request body expecting list with dicts."},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
|
|
try:
|
|
item: Condition = Condition.model_validate(data)
|
|
except ValidationError as exc:
|
|
return web.json_response(
|
|
data={"error": "Failed to validate condition.", "detail": format_validation_errors(exc)},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
|
|
try:
|
|
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)
|
|
|
|
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", r"api/conditions/{id:\d+}", name="condition_get")
|
|
async def conditions_get(request: Request, encoder: Encoder) -> Response:
|
|
"""
|
|
Get the conditions
|
|
|
|
Args:
|
|
request (Request): The request object.
|
|
encoder (Encoder): The encoder instance.
|
|
|
|
Returns:
|
|
Response: The response object.
|
|
|
|
"""
|
|
if not (id := request.match_info.get("id")):
|
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
|
|
|
if not (model := await Conditions.get_instance().get(id)):
|
|
return web.json_response({"error": "Condition not found"}, status=web.HTTPNotFound.status_code)
|
|
|
|
return web.json_response(data=_serialize(model), status=web.HTTPOk.status_code, dumps=encoder.encode)
|
|
|
|
|
|
@route("DELETE", r"api/conditions/{id:\d+}", name="condition_delete")
|
|
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.
|
|
|
|
"""
|
|
if not (id := request.match_info.get("id")):
|
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
|
|
|
try:
|
|
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", r"api/conditions/{id:\d+}", name="condition_patch")
|
|
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.
|
|
|
|
"""
|
|
if not (id := request.match_info.get("id")):
|
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
|
|
|
if not (model := await Conditions.get_instance().get(id)):
|
|
return web.json_response({"error": "Condition not found"}, status=web.HTTPNotFound.status_code)
|
|
|
|
data = await request.json()
|
|
|
|
if not isinstance(data, dict):
|
|
return web.json_response(
|
|
{"error": "Invalid request body expecting list with dicts."},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
|
|
service = Conditions.get_instance()
|
|
|
|
try:
|
|
validated = ConditionPatch.model_validate(data)
|
|
except ValidationError as exc:
|
|
return web.json_response(
|
|
data={"error": "Failed to validate condition.", "detail": format_validation_errors(exc)},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
|
|
if validated.name and await service._repo.get_by_name(validated.name, exclude_id=model.id):
|
|
return web.json_response(
|
|
data={"error": f"Condition with name '{validated.name}' already exists."},
|
|
status=web.HTTPConflict.status_code,
|
|
)
|
|
|
|
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", r"api/conditions/{id:\d+}", name="condition_update")
|
|
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.
|
|
|
|
"""
|
|
if not (id := request.match_info.get("id")):
|
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
|
|
|
if not (model := await Conditions.get_instance().get(id)):
|
|
return web.json_response({"error": "Condition not found"}, status=web.HTTPNotFound.status_code)
|
|
|
|
data = await request.json()
|
|
|
|
if not isinstance(data, dict):
|
|
return web.json_response(
|
|
{"error": "Invalid request body expecting list with dicts."},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
|
|
service = Conditions.get_instance()
|
|
|
|
try:
|
|
validated = Condition.model_validate(data)
|
|
except ValidationError as exc:
|
|
return web.json_response(
|
|
data={"error": "Failed to validate condition.", "detail": format_validation_errors(exc)},
|
|
status=web.HTTPBadRequest.status_code,
|
|
)
|
|
|
|
if validated.name and await service._repo.get_by_name(validated.name, exclude_id=model.id):
|
|
return web.json_response(
|
|
data={"error": f"Condition with name '{validated.name}' already exists."},
|
|
status=web.HTTPConflict.status_code,
|
|
)
|
|
|
|
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)
|