first version

This commit is contained in:
Lukas Pupka-Lipinski 2025-05-01 21:42:57 +02:00
commit 1bc13e7cb0
8 changed files with 263 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.idea/*
/aniworld/bin/*
/aniworld/lib/*

21
Dockerfile Normal file
View File

@ -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"]

89
Loader.py Normal file
View File

@ -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}")

Binary file not shown.

5
aniworld/pyvenv.cfg Normal file
View File

@ -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

98
main.py Normal file
View File

@ -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()

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
aniworld
requests
beautifulsoup4
lxml
python-dotenv

42
vpn_monitor.sh Normal file
View File

@ -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