Fix: Remove episodes from missing list on download/rescan
- Update _update_series_in_db to sync missing episodes bidirectionally - Add delete_by_series_and_episode method to EpisodeService - Remove downloaded episodes from DB after successful download - Clear anime service cache when episodes are removed - Fix tests to use 'message' instead of 'detail' in API responses - Mock DB operations in rescan tests
This commit is contained in:
@@ -397,32 +397,65 @@ class AnimeService:
|
||||
)
|
||||
|
||||
async def _update_series_in_db(self, serie, existing, db) -> None:
|
||||
"""Update an existing series in the database."""
|
||||
"""Update an existing series in the database.
|
||||
|
||||
Syncs the database episodes with the current missing episodes from scan.
|
||||
- Adds new missing episodes that are not in the database
|
||||
- Removes episodes from database that are no longer missing
|
||||
(i.e., the file has been added to the filesystem)
|
||||
"""
|
||||
from src.server.database.service import AnimeSeriesService, EpisodeService
|
||||
|
||||
# Get existing episodes
|
||||
# Get existing episodes from database
|
||||
existing_episodes = await EpisodeService.get_by_series(db, existing.id)
|
||||
existing_dict: dict[int, list[int]] = {}
|
||||
|
||||
# Build dict of existing episodes: {season: {ep_num: episode_id}}
|
||||
existing_dict: dict[int, dict[int, int]] = {}
|
||||
for ep in existing_episodes:
|
||||
if ep.season not in existing_dict:
|
||||
existing_dict[ep.season] = []
|
||||
existing_dict[ep.season].append(ep.episode_number)
|
||||
for season in existing_dict:
|
||||
existing_dict[season].sort()
|
||||
existing_dict[ep.season] = {}
|
||||
existing_dict[ep.season][ep.episode_number] = ep.id
|
||||
|
||||
# Update episodes if changed
|
||||
if existing_dict != serie.episodeDict:
|
||||
new_dict = serie.episodeDict or {}
|
||||
for season, episode_numbers in new_dict.items():
|
||||
existing_eps = set(existing_dict.get(season, []))
|
||||
for ep_num in episode_numbers:
|
||||
if ep_num not in existing_eps:
|
||||
await EpisodeService.create(
|
||||
db=db,
|
||||
series_id=existing.id,
|
||||
season=season,
|
||||
episode_number=ep_num,
|
||||
)
|
||||
# Get new missing episodes from scan
|
||||
new_dict = serie.episodeDict or {}
|
||||
|
||||
# Build set of new missing episodes for quick lookup
|
||||
new_missing_set: set[tuple[int, int]] = set()
|
||||
for season, episode_numbers in new_dict.items():
|
||||
for ep_num in episode_numbers:
|
||||
new_missing_set.add((season, ep_num))
|
||||
|
||||
# Add new missing episodes that are not in the database
|
||||
for season, episode_numbers in new_dict.items():
|
||||
existing_season_eps = existing_dict.get(season, {})
|
||||
for ep_num in episode_numbers:
|
||||
if ep_num not in existing_season_eps:
|
||||
await EpisodeService.create(
|
||||
db=db,
|
||||
series_id=existing.id,
|
||||
season=season,
|
||||
episode_number=ep_num,
|
||||
)
|
||||
logger.debug(
|
||||
"Added missing episode to database: %s S%02dE%02d",
|
||||
serie.key,
|
||||
season,
|
||||
ep_num
|
||||
)
|
||||
|
||||
# Remove episodes from database that are no longer missing
|
||||
# (i.e., the episode file now exists on the filesystem)
|
||||
for season, eps_dict in existing_dict.items():
|
||||
for ep_num, episode_id in eps_dict.items():
|
||||
if (season, ep_num) not in new_missing_set:
|
||||
await EpisodeService.delete(db, episode_id)
|
||||
logger.info(
|
||||
"Removed episode from database (no longer missing): "
|
||||
"%s S%02dE%02d",
|
||||
serie.key,
|
||||
season,
|
||||
ep_num
|
||||
)
|
||||
|
||||
# Update folder if changed
|
||||
if existing.folder != serie.folder:
|
||||
|
||||
@@ -201,7 +201,58 @@ class DownloadService:
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete from database: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
async def _remove_episode_from_missing_list(
|
||||
self,
|
||||
series_key: str,
|
||||
season: int,
|
||||
episode: int,
|
||||
) -> bool:
|
||||
"""Remove a downloaded episode from the missing episodes list.
|
||||
|
||||
Called when a download completes successfully to update the
|
||||
database so the episode no longer appears as missing.
|
||||
|
||||
Args:
|
||||
series_key: Unique provider key for the series
|
||||
season: Season number
|
||||
episode: Episode number within season
|
||||
|
||||
Returns:
|
||||
True if episode was removed, False otherwise
|
||||
"""
|
||||
try:
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.service import EpisodeService
|
||||
|
||||
async with get_db_session() as db:
|
||||
deleted = await EpisodeService.delete_by_series_and_episode(
|
||||
db=db,
|
||||
series_key=series_key,
|
||||
season=season,
|
||||
episode_number=episode,
|
||||
)
|
||||
if deleted:
|
||||
logger.info(
|
||||
"Removed episode from missing list: "
|
||||
"%s S%02dE%02d",
|
||||
series_key,
|
||||
season,
|
||||
episode,
|
||||
)
|
||||
# Clear the anime service cache so list_missing
|
||||
# returns updated data
|
||||
try:
|
||||
self._anime_service._cached_list_missing.cache_clear()
|
||||
except Exception:
|
||||
pass
|
||||
return deleted
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to remove episode from missing list: %s", e
|
||||
)
|
||||
return False
|
||||
|
||||
async def _init_queue_progress(self) -> None:
|
||||
"""Initialize the download queue progress tracking.
|
||||
|
||||
@@ -885,6 +936,13 @@ class DownloadService:
|
||||
# Delete completed item from database (status is in-memory)
|
||||
await self._delete_from_database(item.id)
|
||||
|
||||
# Remove episode from missing episodes list in database
|
||||
await self._remove_episode_from_missing_list(
|
||||
series_key=item.serie_id,
|
||||
season=item.episode.season,
|
||||
episode=item.episode.episode,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Download completed successfully: item_id=%s", item.id
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user