Files
ohm_streaming/app/episode_checker.py
2026-02-28 09:22:57 +00:00

277 lines
9.8 KiB
Python

"""Episode checker for detecting and downloading new episodes automatically"""
import logging
from typing import List, Optional, Dict
from datetime import datetime
from app.watchlist import watchlist_manager, WatchlistManager
from app.models import DownloadRequest, DownloadTask, DownloadStatus
from app.models.watchlist import (
WatchlistItem,
WatchlistSettings,
NewEpisodeInfo,
AutoDownloadResult
)
logger = logging.getLogger(__name__)
class EpisodeChecker:
"""Checks for new episodes and downloads them automatically"""
def __init__(self, wlm: Optional[WatchlistManager] = None):
self.wlm = wlm or watchlist_manager
self.download_manager = None # Will be set by main.py
def set_download_manager(self, download_manager):
"""Set the download manager (called by main.py to avoid circular import)"""
self.download_manager = download_manager
async def check_anime(self, item: WatchlistItem) -> List[NewEpisodeInfo]:
"""
Check for new episodes of a specific anime
Args:
item: WatchlistItem to check
Returns:
List of NewEpisodeInfo objects
"""
try:
logger.info(f"Checking for new episodes: {item.anime_title}")
# Import here to avoid circular imports
from app.downloaders import get_downloader
from urllib.parse import unquote
# Decode URL if it's encoded (handles double-encoded URLs)
anime_url = item.anime_url
try:
# Try to decode - if already decoded, this will be a no-op
decoded_url = unquote(anime_url)
# Handle double encoding
if '%' in decoded_url:
decoded_url = unquote(decoded_url)
anime_url = decoded_url
except Exception as e:
logger.warning(f"Could not decode URL: {e}, using original")
# Get the appropriate downloader
downloader = get_downloader(anime_url)
if not downloader:
logger.error(f"No downloader found for URL: {anime_url}")
return []
# Get episodes list
episodes = await downloader.get_episodes(anime_url, item.lang)
if not episodes:
logger.warning(f"No episodes found for {item.anime_title}")
return []
# Filter new episodes
new_episodes = []
for ep in episodes:
# Handle both 'episode' (from anime-sama) and 'episode_number' keys
ep_num_raw = ep.get('episode_number') or ep.get('episode')
# Convert to int (handles string episode numbers like "01", "02")
try:
ep_num = int(str(ep_num_raw).lstrip('0') or '0')
except (ValueError, TypeError):
ep_num = 0
if ep_num > item.last_episode_downloaded:
new_episodes.append(NewEpisodeInfo(
episode_number=ep_num,
episode_title=ep.get('title'),
episode_url=ep['url'],
season_number=ep.get('season'),
anime_title=item.anime_title,
provider_id=item.provider_id
))
if new_episodes:
logger.info(f"Found {len(new_episodes)} new episodes for {item.anime_title}")
else:
logger.info(f"No new episodes for {item.anime_title}")
return new_episodes
except Exception as e:
logger.error(f"Error checking anime {item.anime_title}: {e}", exc_info=True)
return []
async def download_new_episodes(
self,
item: WatchlistItem,
episodes: List[NewEpisodeInfo]
) -> AutoDownloadResult:
"""
Download new episodes for a watchlist item
Args:
item: WatchlistItem
episodes: List of new episodes to download
Returns:
AutoDownloadResult with download status
"""
result = AutoDownloadResult(
watchlist_item_id=item.id,
anime_title=item.anime_title,
new_episodes_found=len(episodes),
checked_at=datetime.now()
)
if not episodes:
return result
# Get settings
settings = self.wlm.get_settings()
if not settings.auto_download_enabled:
logger.info(f"Auto-download disabled, skipping {len(episodes)} episodes")
return result
try:
# Import here to avoid circular imports
from app.downloaders import get_downloader
from urllib.parse import unquote
# Decode URL if it's encoded
anime_url = item.anime_url
try:
decoded_url = unquote(anime_url)
if '%' in decoded_url:
decoded_url = unquote(decoded_url)
anime_url = decoded_url
except Exception:
pass
downloader = get_downloader(anime_url)
# Download each new episode
for ep_info in episodes:
try:
logger.info(f"Downloading {item.anime_title} Episode {ep_info.episode_number}")
# Get download link - episode_url may be pipe-separated with multiple sources
download_link, filename = await downloader.get_download_link(ep_info.episode_url)
# Create download task
request = DownloadRequest(url=download_link, filename=filename)
task = self.download_manager.create_task(request)
if task:
await self.download_manager.start_download(task.id)
result.episodes_downloaded.append(ep_info.episode_number)
logger.info(f"Started download: {filename}")
else:
result.episodes_failed.append((ep_info.episode_number, "Failed to create download task"))
except Exception as e:
error_msg = str(e)
logger.error(f"Error downloading episode {ep_info.episode_number}: {error_msg}")
result.episodes_failed.append((ep_info.episode_number, error_msg))
# Update watchlist with last episode downloaded
if result.episodes_downloaded:
last_ep = max(result.episodes_downloaded)
self.wlm.update_check_time(item.id, last_ep)
except Exception as e:
logger.error(f"Error in download_new_episodes: {e}", exc_info=True)
return result
async def check_and_download(self, item: WatchlistItem) -> AutoDownloadResult:
"""
Check for new episodes and download them if auto_download is enabled
Args:
item: WatchlistItem to check
Returns:
AutoDownloadResult
"""
# Check for new episodes
new_episodes = await self.check_anime(item)
result = AutoDownloadResult(
watchlist_item_id=item.id,
anime_title=item.anime_title,
new_episodes_found=len(new_episodes),
checked_at=datetime.now()
)
# Download if auto_download is enabled
if item.auto_download and new_episodes:
settings = self.wlm.get_settings()
if settings.auto_download_enabled:
download_result = await self.download_new_episodes(item, new_episodes)
result = download_result
else:
logger.info(f"Auto-download globally disabled, skipping {len(new_episodes)} episodes")
# Update check time even if no downloads
self.wlm.update_check_time(item.id, item.last_episode_downloaded)
return result
async def check_all_due(self) -> List[AutoDownloadResult]:
"""
Check all watchlist items that are due for checking
Returns:
List of AutoDownloadResult objects
"""
settings = self.wlm.get_settings()
due_items = self.wlm.get_due_for_check(settings.check_interval_hours)
logger.info(f"Checking {len(due_items)} due watchlist items")
results = []
for item in due_items:
try:
result = await self.check_and_download(item)
results.append(result)
except Exception as e:
logger.error(f"Error processing {item.anime_title}: {e}", exc_info=True)
# Still add a result to track the failure
results.append(AutoDownloadResult(
watchlist_item_id=item.id,
anime_title=item.anime_title,
new_episodes_found=0,
checked_at=datetime.now()
))
# Log summary
total_new = sum(r.new_episodes_found for r in results)
total_downloaded = sum(len(r.episodes_downloaded) for r in results)
total_failed = sum(len(r.episodes_failed) for r in results)
logger.info(
f"Check complete: {total_new} new episodes found, "
f"{total_downloaded} downloaded, {total_failed} failed"
)
return results
async def manual_check(self, item_id: str) -> Optional[AutoDownloadResult]:
"""
Manually trigger a check for a specific watchlist item
Args:
item_id: Watchlist item ID
Returns:
AutoDownloadResult or None if item not found
"""
item = self.wlm.get_by_id(item_id)
if not item:
logger.error(f"Watchlist item not found: {item_id}")
return None
return await self.check_and_download(item)
# Global episode checker instance
episode_checker = EpisodeChecker()