added delete option
This commit is contained in:
@@ -1634,6 +1634,200 @@ class AnimeService:
|
||||
logger.error("Failed to get NFO statistics: %s", str(exc))
|
||||
raise AnimeServiceError("NFO statistics query failed") from exc
|
||||
|
||||
async def delete_series(
|
||||
self,
|
||||
key: str,
|
||||
delete_database: bool = True,
|
||||
delete_folder: bool = False,
|
||||
) -> "DeleteSeriesResult":
|
||||
"""Delete an anime series from database, filesystem, or both.
|
||||
|
||||
Args:
|
||||
key: Series key (primary identifier)
|
||||
delete_database: If True, remove from database (default True)
|
||||
delete_folder: If True, remove folder from filesystem (default False)
|
||||
|
||||
Returns:
|
||||
DeleteSeriesResult with success status, what was deleted, errors
|
||||
"""
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
from src.server.models.anime import DeleteSeriesResult
|
||||
from src.server.utils.filesystem import is_safe_path
|
||||
import os as _os
|
||||
import shutil
|
||||
|
||||
logger.info(
|
||||
"Delete series requested: key=%s delete_database=%s delete_folder=%s",
|
||||
key, delete_database, delete_folder,
|
||||
)
|
||||
|
||||
# Guard: at least one deletion mode must be selected
|
||||
if not delete_database and not delete_folder:
|
||||
logger.warning(
|
||||
"Delete series rejected - no options selected: key=%s", key
|
||||
)
|
||||
return DeleteSeriesResult(
|
||||
success=False,
|
||||
key=key,
|
||||
name="",
|
||||
folder_path=None,
|
||||
deleted_from_database=False,
|
||||
deleted_folder=False,
|
||||
database_error=None,
|
||||
folder_error=None,
|
||||
message="At least one of delete_database or delete_folder must be True.",
|
||||
)
|
||||
|
||||
# Single DB session for fetch + optional delete
|
||||
async with get_db_session() as db:
|
||||
series = await AnimeSeriesService.get_by_key(db, key)
|
||||
if not series:
|
||||
logger.warning(
|
||||
"Delete series failed - not found: key=%s", key
|
||||
)
|
||||
return DeleteSeriesResult(
|
||||
success=False,
|
||||
key=key,
|
||||
name="",
|
||||
folder_path=None,
|
||||
deleted_from_database=False,
|
||||
deleted_folder=False,
|
||||
database_error=None,
|
||||
folder_error=None,
|
||||
message=f"Series '{key}' not found.",
|
||||
)
|
||||
|
||||
series_id = series.id
|
||||
series_name = series.name
|
||||
folder_path = series.folder
|
||||
|
||||
result = DeleteSeriesResult(
|
||||
success=True,
|
||||
key=key,
|
||||
name=series_name,
|
||||
folder_path=folder_path,
|
||||
message="",
|
||||
)
|
||||
|
||||
# --- Database deletion ---
|
||||
if delete_database:
|
||||
try:
|
||||
async with get_db_session() as db:
|
||||
deleted = await AnimeSeriesService.delete(db, series_id)
|
||||
if deleted:
|
||||
logger.info(
|
||||
"Deleted series from database: key=%s name=%s id=%d",
|
||||
key, series_name, series_id,
|
||||
)
|
||||
result.deleted_from_database = True
|
||||
else:
|
||||
# Already gone is treated as success
|
||||
result.deleted_from_database = True
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to delete series from database: key=%s error=%s",
|
||||
key, str(exc),
|
||||
)
|
||||
result.database_error = str(exc)
|
||||
result.success = False
|
||||
|
||||
# Invalidate list cache
|
||||
try:
|
||||
self._cached_list_missing.cache_clear()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
# Broadcast deletion via WebSocket
|
||||
try:
|
||||
await self._broadcast_series_deleted(key, series_name)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to broadcast series_deleted for %s: %s",
|
||||
key, exc,
|
||||
)
|
||||
|
||||
# --- Filesystem deletion ---
|
||||
if delete_folder and folder_path:
|
||||
# Resolve absolute path and validate it is within base directory
|
||||
abs_folder = _os.path.abspath(folder_path)
|
||||
base_dir = _os.path.abspath(self._directory)
|
||||
|
||||
if not is_safe_path(base_dir, abs_folder):
|
||||
logger.warning(
|
||||
"Blocked unsafe folder delete attempt: key=%s path=%s base=%s",
|
||||
key, abs_folder, base_dir,
|
||||
)
|
||||
result.folder_error = (
|
||||
f"Path '{abs_folder}' is outside the anime directory "
|
||||
f"'{base_dir}' and will not be deleted."
|
||||
)
|
||||
result.success = False
|
||||
elif not _os.path.isdir(abs_folder):
|
||||
logger.warning(
|
||||
"Delete folder skipped - path does not exist: key=%s path=%s",
|
||||
key, abs_folder,
|
||||
)
|
||||
# Not an error; folder might never have existed
|
||||
else:
|
||||
try:
|
||||
logger.info(
|
||||
"Deleting series folder: key=%s path=%s",
|
||||
key, abs_folder,
|
||||
)
|
||||
shutil.rmtree(abs_folder)
|
||||
logger.info(
|
||||
"Deleted series folder: key=%s path=%s",
|
||||
key, abs_folder,
|
||||
)
|
||||
result.deleted_folder = True
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to delete series folder: key=%s path=%s error=%s",
|
||||
key, abs_folder, str(exc),
|
||||
)
|
||||
result.folder_error = str(exc)
|
||||
result.success = False
|
||||
|
||||
# --- Build message ---
|
||||
parts = []
|
||||
if result.deleted_from_database and not result.database_error:
|
||||
parts.append("removed from database")
|
||||
if result.deleted_folder and not result.folder_error:
|
||||
parts.append("folder deleted from filesystem")
|
||||
if result.database_error:
|
||||
parts.append(f"database error: {result.database_error}")
|
||||
if result.folder_error:
|
||||
parts.append(f"folder error: {result.folder_error}")
|
||||
|
||||
if parts:
|
||||
result.message = "; ".join(parts)
|
||||
else:
|
||||
result.message = "No action taken."
|
||||
|
||||
logger.info(
|
||||
"Delete series completed: key=%s deleted_db=%s deleted_folder=%s",
|
||||
key, result.deleted_from_database, result.deleted_folder,
|
||||
)
|
||||
return result
|
||||
|
||||
async def _broadcast_series_deleted(self, key: str, name: str) -> None:
|
||||
"""Broadcast series_deleted event via WebSocket."""
|
||||
try:
|
||||
await self._websocket_service.broadcast_series_deleted(
|
||||
key=key,
|
||||
name=name,
|
||||
)
|
||||
logger.info(
|
||||
"series_deleted broadcast sent: key=%s name=%s",
|
||||
key, name,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to broadcast series_deleted: key=%s error=%s",
|
||||
key, str(exc),
|
||||
)
|
||||
|
||||
|
||||
def get_anime_service(series_app: SeriesApp) -> AnimeService:
|
||||
"""Factory used for creating AnimeService with a SeriesApp instance."""
|
||||
|
||||
Reference in New Issue
Block a user