333 lines
8.8 KiB
Python
333 lines
8.8 KiB
Python
import json
|
|
import logging
|
|
import re
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from aiohttp import web
|
|
|
|
from .config import Config
|
|
from .encoder import Encoder
|
|
from .Events import EventBus, Events
|
|
from .Singleton import Singleton
|
|
from .Utils import init_class
|
|
|
|
LOG = logging.getLogger("DLFields")
|
|
|
|
|
|
class FieldType(str, Enum):
|
|
STRING = "string"
|
|
TEXT = "text"
|
|
BOOL = "bool"
|
|
|
|
@classmethod
|
|
def all(cls) -> list[str]:
|
|
return [member.value for member in cls]
|
|
|
|
@classmethod
|
|
def from_value(cls, value: str) -> "FieldType":
|
|
"""
|
|
Returns the FieldType enum member corresponding to the given value.
|
|
|
|
Args:
|
|
value (str): The value to match against the enum members.
|
|
|
|
Returns:
|
|
StoreType: The enum member that matches the value.
|
|
|
|
Raises:
|
|
ValueError: If the value does not match any member.
|
|
|
|
"""
|
|
for member in cls:
|
|
if member.value == value:
|
|
return member
|
|
|
|
msg = f"Invalid StoreType value: {value}"
|
|
raise ValueError(msg)
|
|
|
|
def __str__(self) -> str:
|
|
return self.value
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class DLField:
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
"""The id of the field."""
|
|
|
|
name: str
|
|
"""The name of the preset."""
|
|
|
|
description: str
|
|
"""The description of the preset."""
|
|
|
|
field: str
|
|
"""The yt-dlp field to use in long format."""
|
|
|
|
kind: FieldType = FieldType.TEXT
|
|
"""The kind of the field. i.e. string, bool"""
|
|
|
|
icon: str = ""
|
|
"""The icon of the field, it can be a font-awesome icon"""
|
|
|
|
order: int = 0
|
|
"""The order of the field, used to sort the fields in the UI."""
|
|
|
|
value: str = ""
|
|
"""The default value of the field, It's currently unused."""
|
|
|
|
extras: dict = field(default_factory=dict)
|
|
"""Additional options for the field."""
|
|
|
|
def serialize(self) -> dict:
|
|
dct = self.__dict__
|
|
dct["kind"] = str(self.kind)
|
|
dct["extras"] = {k: v for k, v in self.extras.items() if v is not None}
|
|
return dct
|
|
|
|
def json(self) -> str:
|
|
return Encoder().encode(self.serialize())
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
return self.serialize().get(key, default)
|
|
|
|
|
|
class DLFields(metaclass=Singleton):
|
|
"""
|
|
This class is used to manage the DLFields.
|
|
"""
|
|
|
|
def __init__(self, file: str | Path | None = None, config: Config | None = None):
|
|
self._items: list[DLField] = []
|
|
"The list of items."
|
|
config: Config = config or Config.get_instance()
|
|
"The configuration instance."
|
|
self._file: Path = Path(file) if file else Path(config.config_path).joinpath("dl_fields.json")
|
|
"The path to the file where the items are stored."
|
|
|
|
if self._file.exists() and "600" != self._file.stat().st_mode:
|
|
try:
|
|
self._file.chmod(0o600)
|
|
except Exception:
|
|
pass
|
|
|
|
@staticmethod
|
|
def get_instance(file: str | Path | None = None, config: Config | None = None) -> "DLFields":
|
|
"""
|
|
Get the instance of the class.
|
|
|
|
Returns:
|
|
DLFields: The instance of the class
|
|
|
|
"""
|
|
return DLFields(file=file, config=config)
|
|
|
|
async def on_shutdown(self, _: web.Application):
|
|
pass
|
|
|
|
def attach(self, _: web.Application):
|
|
"""
|
|
Attach the class to the aiohttp application.
|
|
|
|
Args:
|
|
_ (web.Application): The aiohttp application.
|
|
|
|
Returns:
|
|
None
|
|
|
|
"""
|
|
self.load()
|
|
|
|
async def event_handler(_, __):
|
|
msg = "Not implemented"
|
|
raise Exception(msg)
|
|
|
|
EventBus.get_instance().subscribe(Events.DLFIELDS_ADD, event_handler, f"{__class__.__name__}.add")
|
|
|
|
def get_all(self) -> list[DLField]:
|
|
"""Return the items."""
|
|
return self._items
|
|
|
|
def load(self) -> "DLFields":
|
|
"""
|
|
Load the items.
|
|
|
|
Returns:
|
|
Presets: The current instance.
|
|
|
|
"""
|
|
has: int = len(self._items)
|
|
self.clear()
|
|
|
|
if not self._file.exists() or self._file.stat().st_size < 10:
|
|
return self
|
|
|
|
try:
|
|
LOG.info(f"{'Reloading' if has else 'Loading'} '{self._file}'.")
|
|
items: dict = json.loads(self._file.read_text())
|
|
except Exception as e:
|
|
LOG.error(f"Failed to parse '{self._file}'. '{e}'.")
|
|
return self
|
|
|
|
if not items or len(items) < 1:
|
|
return self
|
|
|
|
need_save = False
|
|
|
|
for i, item in enumerate(items):
|
|
try:
|
|
if "id" not in item:
|
|
item["id"] = str(uuid.uuid4())
|
|
need_save = True
|
|
|
|
item: DLField = init_class(DLField, item)
|
|
|
|
self._items.append(item)
|
|
except Exception as e:
|
|
LOG.error(f"Failed to parse '{self._file}:{i}'. '{e!s}'.")
|
|
continue
|
|
|
|
if need_save:
|
|
LOG.info(f"Saving '{self._file}'.")
|
|
self.save(self._items)
|
|
|
|
return self
|
|
|
|
def clear(self) -> "DLFields":
|
|
"""
|
|
Clear all items.
|
|
|
|
Returns:
|
|
DLFields: The current instance.
|
|
|
|
"""
|
|
if len(self._items) < 1:
|
|
return self
|
|
|
|
self._items.clear()
|
|
|
|
return self
|
|
|
|
def validate(self, item: DLField | dict) -> bool:
|
|
"""
|
|
Validate the item.
|
|
|
|
Args:
|
|
item (DLField|dict): The item to validate.
|
|
|
|
Returns:
|
|
bool: True if valid
|
|
|
|
Raises:
|
|
ValueError: If the item is not valid.
|
|
|
|
"""
|
|
if not isinstance(item, dict):
|
|
if not isinstance(item, DLField):
|
|
msg = f"Unexpected '{type(item).__name__}' type was given."
|
|
raise ValueError(msg)
|
|
|
|
item = item.serialize()
|
|
|
|
for key in ["id", "name", "description", "field", "kind"]:
|
|
if key not in item:
|
|
msg = f"Missing required key '{key}'."
|
|
raise ValueError(msg)
|
|
|
|
if item.get("kind") not in FieldType.all():
|
|
msg = f"Invalid field type '{item.get('kind')}'."
|
|
raise ValueError(msg)
|
|
|
|
if item.get("extras") and not isinstance(item.get("extras"), dict):
|
|
msg = "Extras must be a dictionary."
|
|
raise ValueError(msg)
|
|
|
|
if item.get("value") and not isinstance(item.get("value"), str):
|
|
msg = "Value must be a string."
|
|
raise ValueError(msg)
|
|
|
|
if item.get("order") is not None and not isinstance(item.get("order"), int):
|
|
msg = "Order must be an integer."
|
|
raise ValueError(msg)
|
|
|
|
if not isinstance(item.get("extras", {}), dict):
|
|
msg = "Extras must be a dictionary."
|
|
raise ValueError(msg)
|
|
|
|
if re.match(r"^--[a-zA-Z0-9\-]+$", item.get("field", "").strip()) is None:
|
|
msg = "Invalid yt-dlp option field it must starts with '--' and contain only alphanumeric characters."
|
|
raise ValueError(msg)
|
|
|
|
return True
|
|
|
|
def save(self, items: list[DLField | dict]) -> "DLFields":
|
|
"""
|
|
Save the items.
|
|
|
|
Args:
|
|
items (list[DLField]): The items to save.
|
|
|
|
Returns:
|
|
Presets: The current instance.
|
|
|
|
"""
|
|
for i, item in enumerate(items):
|
|
try:
|
|
if not isinstance(item, DLField):
|
|
item: DLField = init_class(DLField, item)
|
|
items[i] = item
|
|
except Exception as e:
|
|
LOG.error(f"Failed to save item '{i}' due to parsing error. '{e!s}'.")
|
|
continue
|
|
|
|
try:
|
|
self.validate(item)
|
|
except ValueError as e:
|
|
LOG.error(f"Failed to validate item '{i}: {item.name}'. '{e}'.")
|
|
continue
|
|
|
|
try:
|
|
self._file.write_text(json.dumps(obj=[item.serialize() for item in items], indent=4))
|
|
LOG.info(f"Saved '{self._file}'.")
|
|
except Exception as e:
|
|
LOG.error(f"Failed to save '{self._file}'. '{e!s}'.")
|
|
|
|
return self
|
|
|
|
def has(self, id_or_name: str) -> bool:
|
|
"""
|
|
Check if the item exists by id or name.
|
|
|
|
Args:
|
|
id_or_name (str): The id or name of the item.
|
|
|
|
Returns:
|
|
bool: True if exists, False otherwise.
|
|
|
|
"""
|
|
return self.get(id_or_name) is not None
|
|
|
|
def get(self, id_or_name: str) -> DLField | None:
|
|
"""
|
|
Get the item by id or name.
|
|
|
|
Args:
|
|
id_or_name (str): The id or name of the item.
|
|
|
|
Returns:
|
|
Preset|None: The item if found, None otherwise.
|
|
|
|
"""
|
|
if not id_or_name:
|
|
return None
|
|
|
|
for item in self.get_all():
|
|
if id_or_name not in (item.id, item.name):
|
|
continue
|
|
|
|
return item
|
|
|
|
return None
|