import sys import os import traceback import re import logging from concurrent.futures import ThreadPoolExecutor from collections import defaultdict from aniworld.models import Anime, Episode from aniworld.common import get_season_episode_count, get_movie_episode_count from aniworld.search import search_anime from Loader import download # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s') console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(logging.Formatter( "%(asctime)s - %(levelname)s - %(funcName)s - %(message)s") ) logging.getLogger().addHandler(console_handler) logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO) logging.getLogger('charset_normalizer').setLevel(logging.INFO) logging.getLogger().setLevel(logging.INFO) error_logger = logging.getLogger("ErrorLog") error_handler = logging.FileHandler("errors.log") error_handler.setLevel(logging.ERROR) error_logger.addHandler(error_handler) noKeyFound_logger = logging.getLogger("NoKeyFound") noKeyFound_handler = logging.FileHandler("NoKeyFound.log") noKeyFound_handler.setLevel(logging.ERROR) noKeyFound_logger.addHandler(noKeyFound_handler) noGerFound_logger = logging.getLogger("noGerFound") noGerFound_handler = logging.FileHandler("noGerFound.log") noGerFound_handler.setLevel(logging.ERROR) noGerFound_logger.addHandler(noGerFound_handler) class NoKeyFoundException(Exception): """Exception raised when an anime key cannot be found.""" pass class MatchNotFoundError(Exception): """Exception raised when an anime key cannot be found.""" pass class Loader: def __init__(self, basePath: str): self.directory = basePath logging.info(f"Initialized Loader with base path: {self.directory}") def __find_mp4_files(self): logging.info("Scanning for .mp4 files") for root_folder_name in os.listdir(self.directory): folder_data = defaultdict(list) # Dictionary to store MP4 files per folder folder = os.path.join(self.directory, root_folder_name) logging.info(f"Processing folder: {root_folder_name}") # First pass: Scan all folders and collect MP4 file data for root, dirs, files in os.walk(folder): mp4_files = [file for file in files if file.endswith('.mp4')] if mp4_files: folder_data[root_folder_name].extend(mp4_files) yield root_folder_name, folder_data[root_folder_name] for dir in self.__find_empty_folders(): logging.info(f"Found no .mp4 files in {dir}") yield dir, [] def __find_empty_folders(self): """Yield folder names that do not contain any mp4 files in a given directory.""" for folder in os.listdir(self.directory): folder_path = os.path.join(self.directory, folder) if os.path.isdir(folder_path): # Ensure it's a directory has_mp4 = any(file.endswith(".mp4") for file in os.listdir(folder_path)) if not has_mp4: yield folder # Yield the folder name if no mp4 files found def __remove_year(self, input_string: str): cleaned_string = re.sub(r'\(\d{4}\)', '', input_string).strip() logging.debug(f"Removed year from '{input_string}' -> '{cleaned_string}'") return cleaned_string def __check_and_generate_key(self, folder_name: str): folder_path = os.path.join(self.directory, folder_name) key_file = os.path.join(folder_path, 'key') if os.path.exists(key_file): with open(key_file, 'r') as file: key = file.read().strip() logging.info(f"Key found for folder '{folder_name}': {key}") return key else: try: key = search_anime(folder_name, True) if key: key = key[0]['link'] with open(key_file, 'w') as file: file.write(key) logging.info(f"Generated new key for folder '{folder_name}': {key}") return key else: raise NoKeyFoundException(f"No key found for folder '{folder_name}'") except Exception as e: raise NoKeyFoundException(f"Failed to retrieve key for folder '{folder_name}'") from e def __GetEpisodeAndSeason(self, filename: str): pattern = r'S(\d+)E(\d+)' match = re.search(pattern, filename) if match: season = match.group(1) episode = match.group(2) logging.debug(f"Extracted season {season}, episode {episode} from '{filename}'") return int(season), int(episode) else: logging.error(f"Failed to find season/episode pattern in '{filename}'") raise MatchNotFoundError("Season and episode pattern not found in the filename.") def __GetEpisodesAndSeasons(self, mp4_files: []): episodes_dict = {} for file in mp4_files: season, episode = self.__GetEpisodeAndSeason(file) if season in episodes_dict: episodes_dict[season].append(episode) else: episodes_dict[season] = [episode] return episodes_dict def __GetMissingEpisodesAndSeason(self, key: str, mp4_files: []): expected_dict = get_season_episode_count(key) # key season , value count of episodes filedict = self.__GetEpisodesAndSeasons(mp4_files) for season, expected_count in expected_dict.items(): existing_episodes = filedict.get(season, []) missing_episodes = [ep for ep in range(1, expected_count + 1) if ep not in existing_episodes] if missing_episodes: yield season, missing_episodes def LoadMissing(self): logging.info("Starting process to load missing episodes") result = self.__find_mp4_files() def download_episode(folder, season, episode, key): """Helper function to download an individual episode.""" try: folder_path = os.path.join(self.directory, folder, f"Season {season}") anime = Anime( episode_list=[Episode(slug=key, season=season, episode=episode)], language="German Dub", output_directory=folder_path ) logging.info(f"Downloading anime {key} season {season} episode {episode}") download(anime) logging.info(f"Downloading completed anime {key} season {season} episode {episode}") except KeyError as keye: noGerFound_logger.error(f"Language not found for anime: {key} season: {season} episode: {episode}") except Exception as e: logging.error(f"Error downloading episode {episode} of season {season} for anime {key}: {e}") # Using ThreadPoolExecutor to run downloads in parallel with ThreadPoolExecutor(max_workers=2) as executor: # Adjust number of workers as needed for folder, mp4_files in result: try: key = self.__check_and_generate_key(folder) missings = self.__GetMissingEpisodesAndSeason(key, mp4_files) for season, missing_episodes in missings: logging.info(f"Missing episodes for {key}\nSeason {str(season)}: Episodes: " + ",".join(f"{''.join(str(v))}" for v in missing_episodes)) for episode in missing_episodes: executor.submit(download_episode, folder, season, episode, key) #download_episode(folder, season, episode, key) except NoKeyFoundException as nkfe: noKeyFound_logger.error(f"Error processing folder '{folder}': {nkfe}") except Exception as e: error_logger.error(f"Unexpected error processing folder '{folder}': {e} \n {traceback.format_exc()}") continue # Read the base directory from an environment variable directory_to_search = os.getenv("ANIME_DIRECTORY", "\\\\sshfs.r\\ubuntu@192.168.178.43\\media\\serien\\Serien") #directory_to_search = os.getenv("ANIME_DIRECTORY", "D:\sss") loader = Loader(directory_to_search) loader.LoadMissing()