first version
This commit is contained in:
commit
1bc13e7cb0
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/.idea/*
|
||||||
|
/aniworld/bin/*
|
||||||
|
/aniworld/lib/*
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal 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
89
Loader.py
Normal 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}")
|
||||||
|
|
||||||
BIN
aniworld-3.0.2-py3-none-any.whl
Normal file
BIN
aniworld-3.0.2-py3-none-any.whl
Normal file
Binary file not shown.
5
aniworld/pyvenv.cfg
Normal file
5
aniworld/pyvenv.cfg
Normal 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
98
main.py
Normal 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
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
aniworld
|
||||||
|
requests
|
||||||
|
beautifulsoup4
|
||||||
|
lxml
|
||||||
|
python-dotenv
|
||||||
42
vpn_monitor.sh
Normal file
42
vpn_monitor.sh
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user