commit 1bc13e7cb03a07d65e7142ecbac55947c899f643 Author: Lukas Pupka-Lipinski Date: Thu May 1 21:42:57 2025 +0200 first version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c18224 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/* +/aniworld/bin/* +/aniworld/lib/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5a934ab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM ubuntu:latest + +# Install dependencies +RUN apt update && apt install -y \ + openvpn \ + python3-pip \ + protonvpn-cli \ + && rm -rf /var/lib/apt/lists/* + +# Install dependencies if you have a requirements file +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the Python script and monitoring script +COPY main.py /main.py +COPY vpn_monitor.sh /vpn_monitor.sh + +# Ensure scripts are executable +RUN chmod +x /main.py /vpn_monitor.sh + +# Entry point: Start VPN and monitor status +CMD ["bash", "/vpn_monitor.sh"] \ No newline at end of file diff --git a/Loader.py b/Loader.py new file mode 100644 index 0000000..109e80c --- /dev/null +++ b/Loader.py @@ -0,0 +1,89 @@ +import os +import re +import subprocess +import logging + +from aniworld.models import Anime +from aniworld.config import PROVIDER_HEADERS, INVALID_PATH_CHARS +from aniworld.parser import arguments + +# Configure logging +logging.basicConfig( + filename="download.log", + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s" +) + +# Read timeout from environment variable, default to 600 seconds (10 minutes) +timeout = int(os.getenv("DOWNLOAD_TIMEOUT", 600)) + +def download(anime: Anime): # pylint: disable=too-many-branches + for episode in anime: + sanitized_anime_title = ''.join( + char for char in anime.title if char not in INVALID_PATH_CHARS + ) + + if episode.season == 0: + output_file = ( + f"{sanitized_anime_title} - " + f"Movie {episode.episode:02} - " + f"({anime.language}).mp4" + ) + else: + output_file = ( + f"{sanitized_anime_title} - " + f"S{episode.season:02}E{episode.episode:03} - " + f"({anime.language}).mp4" + ) + + output_path = os.path.join(anime.output_directory, output_file) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + logging.info(f"Preparing to download: {output_path}") + + command = [ + "yt-dlp", + episode.get_direct_link(anime.provider, anime.language), + "--fragment-retries", "infinite", + "--concurrent-fragments", "4", + "-o", output_path, + "--quiet", + "--no-warnings", + "--progress" + ] + + if anime.provider in PROVIDER_HEADERS: + for header in PROVIDER_HEADERS[anime.provider]: + command.extend(["--add-header", header]) + + if arguments.only_command: + logging.info( + f"{anime.title} - S{episode.season}E{episode.episode} - ({anime.language}): " + f"{' '.join(str(item) if item is not None else '' for item in command)}" + ) + continue + + try: + + logging.info(f"Starting download to {output_path}...") + subprocess.run(command, check=True, timeout=timeout) + logging.info(f"Download completed: {output_path}") + except subprocess.TimeoutExpired: + logging.error(f"Download timed out after {timeout} seconds: {' '.join(str(item) for item in command)}") + except subprocess.CalledProcessError: + logging.error(f"Error running command: {' '.join(str(item) for item in command)}") + except KeyboardInterrupt: + logging.warning("Download interrupted by user.") + output_dir = os.path.dirname(output_path) + is_empty = True + + for file_name in os.listdir(output_dir): + if re.search(r'\.(part|ytdl|part-Frag\d+)$', file_name): + os.remove(os.path.join(output_dir, file_name)) + else: + is_empty = False + + if is_empty or not os.listdir(output_dir): + os.rmdir(output_dir) + logging.info(f"Removed empty download directory: {output_dir}") + diff --git a/aniworld-3.0.2-py3-none-any.whl b/aniworld-3.0.2-py3-none-any.whl new file mode 100644 index 0000000..dbe6396 Binary files /dev/null and b/aniworld-3.0.2-py3-none-any.whl differ diff --git a/aniworld/pyvenv.cfg b/aniworld/pyvenv.cfg new file mode 100644 index 0000000..a375f98 --- /dev/null +++ b/aniworld/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv /mnt/d/repo/AniWorld/aniworld diff --git a/main.py b/main.py new file mode 100644 index 0000000..088db3f --- /dev/null +++ b/main.py @@ -0,0 +1,98 @@ +import os +import re +import logging +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( + filename="loader.log", + level=logging.DEBUG, + format="%(asctime)s - %(levelname)s - %(message)s" +) + +class MatchNotFoundError(Exception): + """Custom exception raised when the pattern match is not 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, dirs, files in os.walk(self.directory): + mp4_files = [file for file in files if file.endswith('.mp4')] + if mp4_files: + relative_path = os.path.relpath(root, self.directory) + root_folder_name = relative_path.split(os.sep)[0] + logging.debug(f"Found {len(mp4_files)} .mp4 files in {root_folder_name}") + yield root_folder_name, mp4_files + + 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.debug(f"Key found for folder '{folder_name}': {key}") + return key + else: + key = search_anime(self.__remove_year(folder_name)) + with open(key_file, 'w') as file: + file.write(key) + logging.info(f"Generated new key for folder '{folder_name}': {key}") + return key + + def __GetEpisodeAndSeason(self, filename: str): + pattern = r'S(\d{2})E(\d{2})' + 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 season, 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 LoadMissing(self): + logging.info("Starting process to load missing episodes") + result = self.__find_mp4_files() + + 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: + folder_path = os.path.join(self.directory, folder, f"Season {season}") + + for episode in missing_episodes: + anime = Anime( + episode_list=[ + Episode(slug=key, season=season, episode=episode) + ], + language="German Dub", + output_directory=folder_path + ) + logging.info(f"Downloading episode {episode} of season {season} for anime {key}") + download(anime) + except Exception as e: + logging.error(f"Error processing folder '{folder}': {e}") + 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") + +loader = Loader(directory_to_search) +loader.LoadMissing() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..310e1a9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +aniworld +requests +beautifulsoup4 +lxml +python-dotenv \ No newline at end of file diff --git a/vpn_monitor.sh b/vpn_monitor.sh new file mode 100644 index 0000000..42521ea --- /dev/null +++ b/vpn_monitor.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Try connecting to ProtonVPN with retries +MAX_RETRIES=5 +RETRY_DELAY=10 # seconds +TRIES=0 + +while [[ $TRIES -lt $MAX_RETRIES ]]; do + echo "Attempting to connect to ProtonVPN (try #$((TRIES+1)))..." + + protonvpn-cli c -r + + # Check if the connection was successful + if protonvpn-cli status | grep -q "Connected"; then + echo "VPN connected successfully!" + break + fi + + echo "VPN connection failed, retrying in $RETRY_DELAY seconds..." + sleep $RETRY_DELAY + ((TRIES++)) +done + +# If the connection still failed after retries, exit +if ! protonvpn-cli status | grep -q "Connected"; then + echo "Failed to establish VPN connection after $MAX_RETRIES attempts. Exiting..." + exit 1 +fi + +# Start the main Python script in the background +python3 /main.py & +MAIN_PID=$! + +# Monitor VPN connection +while true; do + if ! protonvpn-cli status | grep -q "Connected"; then + echo "VPN disconnected! Stopping main.py..." + kill $MAIN_PID + exit 1 + fi + sleep 5 # Check every 5 seconds +done