185 lines
6.1 KiB
Python
185 lines
6.1 KiB
Python
import copy
|
|
import logging
|
|
from collections import OrderedDict
|
|
from collections.abc import Iterable
|
|
from enum import Enum
|
|
|
|
from .downloads import Download
|
|
from .ItemDTO import ItemDTO
|
|
from .operations import matches_condition
|
|
from .sqlite_store import SqliteStore
|
|
|
|
LOG = logging.getLogger("datastore")
|
|
|
|
|
|
class StoreType(str, Enum):
|
|
HISTORY = "done"
|
|
QUEUE = "queue"
|
|
|
|
@classmethod
|
|
def all(cls) -> list[str]:
|
|
return [member.value for member in cls]
|
|
|
|
@classmethod
|
|
def from_value(cls, value: str) -> "StoreType":
|
|
for member in cls:
|
|
if member.value == value:
|
|
return member
|
|
msg = f"Invalid StoreType value: {value}"
|
|
raise ValueError(msg)
|
|
|
|
def __str__(self) -> str: # pragma: no cover
|
|
return self.value
|
|
|
|
|
|
def _strip_transient_fields(item: ItemDTO) -> ItemDTO:
|
|
stored = copy.deepcopy(item)
|
|
if hasattr(stored, "datetime"):
|
|
try:
|
|
delattr(stored, "datetime")
|
|
except AttributeError:
|
|
pass
|
|
if stored.status == "finished" and hasattr(stored, "live_in"):
|
|
try:
|
|
delattr(stored, "live_in")
|
|
except AttributeError:
|
|
pass
|
|
return stored
|
|
|
|
|
|
class DataStore:
|
|
def __init__(self, type: StoreType, connection: SqliteStore):
|
|
self._type = type
|
|
self._connection: SqliteStore = connection
|
|
self._dict: OrderedDict[str, Download] = OrderedDict()
|
|
|
|
async def load(self) -> None:
|
|
saved = await self._connection.fetch_saved(str(self._type))
|
|
for key, item in saved:
|
|
self._dict[key] = Download(info=item)
|
|
|
|
async def saved_items(self) -> list[tuple[str, ItemDTO]]:
|
|
return await self._connection.fetch_saved(str(self._type))
|
|
|
|
async def exists(self, key: str | None = None, url: str | None = None) -> bool:
|
|
if not key and not url:
|
|
msg = "key or url must be provided."
|
|
raise KeyError(msg)
|
|
|
|
if key and key in self._dict:
|
|
return True
|
|
|
|
if any((key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url) for i in self._dict):
|
|
return True
|
|
|
|
return StoreType.HISTORY == self._type and await self._connection.exists(str(self._type), key=key, url=url)
|
|
|
|
async def get(self, key: str | None = None, url: str | None = None) -> Download:
|
|
if not key and not url:
|
|
msg = "key or url must be provided."
|
|
raise KeyError(msg)
|
|
|
|
for i in self._dict:
|
|
if (key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url):
|
|
return self._dict[i]
|
|
|
|
if StoreType.HISTORY == self._type and (item := await self._connection.get(str(self._type), key=key, url=url)):
|
|
self._dict[item._id] = Download(info=item)
|
|
return self._dict[item._id]
|
|
|
|
msg: str = f"{key=} or {url=} not found."
|
|
raise KeyError(msg)
|
|
|
|
async def get_item(self, **kwargs) -> Download | None:
|
|
if not kwargs:
|
|
return None
|
|
|
|
for i in self._dict:
|
|
if not self._dict[i].info:
|
|
continue
|
|
|
|
info = self._dict[i].info.__dict__
|
|
|
|
if any(matches_condition(key, value, info) for key, value in kwargs.items()):
|
|
return self._dict[i]
|
|
|
|
if StoreType.HISTORY == self._type and (item := await self._connection.get_item(str(self._type), **kwargs)):
|
|
self._dict[item._id] = Download(info=item)
|
|
return self._dict[item._id]
|
|
|
|
return None
|
|
|
|
async def get_by_id(self, id: str) -> Download | None:
|
|
val = self._dict.get(id)
|
|
if val or self._type == StoreType.QUEUE:
|
|
return val
|
|
|
|
if item := await self._connection.get_by_id(str(self._type), id):
|
|
self._dict[item._id] = Download(info=item)
|
|
return self._dict[id]
|
|
|
|
return None
|
|
|
|
def items(self):
|
|
return self._dict.items()
|
|
|
|
async def put(self, value: Download, no_notify: bool = False) -> Download:
|
|
_ = no_notify
|
|
self._dict[value.info._id] = value
|
|
await self._connection.enqueue_upsert(str(self._type), _strip_transient_fields(value.info))
|
|
return self._dict[value.info._id]
|
|
|
|
async def delete(self, key: str) -> None:
|
|
self._dict.pop(key, None)
|
|
await self._connection.enqueue_delete(str(self._type), key)
|
|
|
|
def next(self):
|
|
return next(iter(self._dict.items()))
|
|
|
|
def empty(self) -> bool:
|
|
return 0 == len(self._dict)
|
|
|
|
def has_downloads(self) -> bool:
|
|
return any(self._dict[key].info.auto_start and self._dict[key].started() is False for key in self._dict)
|
|
|
|
def get_next_download(self) -> Download | None:
|
|
for key in self._dict:
|
|
ref = self._dict[key]
|
|
if ref.info.auto_start and not ref.started() and not ref.is_cancelled():
|
|
return ref
|
|
return None
|
|
|
|
async def get_total_count(self, status_filter: str | None = None) -> int:
|
|
return await self._connection.count(str(self._type), status_filter=status_filter)
|
|
|
|
async def get_items_paginated(
|
|
self, page: int = 1, per_page: int = 50, order: str = "DESC", status_filter: str | None = None
|
|
) -> tuple[list[tuple[str, Download]], int, int, int]:
|
|
if page < 1:
|
|
msg = "page must be >= 1"
|
|
raise ValueError(msg)
|
|
|
|
if per_page < 1:
|
|
msg = "per_page must be >= 1"
|
|
raise ValueError(msg)
|
|
|
|
order = order.upper()
|
|
if order not in ("ASC", "DESC"):
|
|
msg = f"order must be 'ASC' or 'DESC', got '{order}'"
|
|
raise ValueError(msg)
|
|
|
|
items, total_items, current_page, total_pages = await self._connection.paginate(
|
|
str(self._type), page, per_page, order, status_filter
|
|
)
|
|
|
|
return [(item_id, Download(info=item)) for item_id, item in items], total_items, current_page, total_pages
|
|
|
|
async def bulk_delete(self, ids: Iterable[str]) -> int:
|
|
deleted = await self._connection.bulk_delete(str(self._type), ids)
|
|
for _id in ids:
|
|
self._dict.pop(_id, None)
|
|
return deleted
|
|
|
|
async def test(self) -> bool:
|
|
await self._connection.count(str(self._type))
|
|
return True
|