added console app
This commit is contained in:
parent
aeed2df7d0
commit
3faa6f9a40
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
/.idea/*
|
/.idea/*
|
||||||
/aniworld/bin/*
|
/aniworld/bin/*
|
||||||
/aniworld/lib/*
|
/aniworld/lib/*
|
||||||
|
/src/__pycache__/*
|
||||||
|
/src/__pycache__/
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 03f08a86338e7b1e97bdf1164809698d9ab18ab7
|
Subproject commit a267efa72a12af19df6641ab2ab39e494a1d0ee8
|
||||||
@ -2,11 +2,16 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
from aniworld.models import Anime
|
import requests
|
||||||
from aniworld.config import PROVIDER_HEADERS, INVALID_PATH_CHARS
|
import html
|
||||||
|
from urllib.parse import quote
|
||||||
|
from Serie import Serie
|
||||||
|
from aniworld.models import Anime, Episode, NoMachingLanguage
|
||||||
|
from aniworld.config import PROVIDER_HEADERS, INVALID_PATH_CHARS, ANIWORLD_TO, session, DEFAULT_REQUEST_TIMEOUT
|
||||||
from aniworld.parser import arguments
|
from aniworld.parser import arguments
|
||||||
|
|
||||||
|
|
||||||
# Read timeout from environment variable, default to 600 seconds (10 minutes)
|
# Read timeout from environment variable, default to 600 seconds (10 minutes)
|
||||||
timeout = int(os.getenv("DOWNLOAD_TIMEOUT", 600))
|
timeout = int(os.getenv("DOWNLOAD_TIMEOUT", 600))
|
||||||
|
|
||||||
@ -15,6 +20,57 @@ download_error_handler = logging.FileHandler("../download_errors.log")
|
|||||||
download_error_handler.setLevel(logging.ERROR)
|
download_error_handler.setLevel(logging.ERROR)
|
||||||
download_error_logger.addHandler(download_error_handler)
|
download_error_logger.addHandler(download_error_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def CreateSerie(searchEntry):
|
||||||
|
return Serie(searchEntry["link"], searchEntry["name"], "aniworld.to", searchEntry["link"], {})
|
||||||
|
|
||||||
|
def search_anime(keyword: str = None) -> str:
|
||||||
|
|
||||||
|
search_url = f"{ANIWORLD_TO}/ajax/seriesSearch?keyword={quote(keyword)}"
|
||||||
|
anime_list = fetch_anime_list(search_url)
|
||||||
|
|
||||||
|
return anime_list
|
||||||
|
|
||||||
|
def fetch_anime_list(url: str) -> list:
|
||||||
|
response = session.get(url, timeout=DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
clean_text = response.text.strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded_data = json.loads(html.unescape(clean_text))
|
||||||
|
return decoded_data if isinstance(decoded_data, list) else []
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
try:
|
||||||
|
# Remove BOM and problematic characters
|
||||||
|
clean_text = clean_text.encode('utf-8').decode('utf-8-sig')
|
||||||
|
# Remove problematic characters
|
||||||
|
clean_text = re.sub(r'[\x00-\x1F\x7F-\x9F]', '', clean_text)
|
||||||
|
# Parse the new text
|
||||||
|
decoded_data = json.loads(clean_text)
|
||||||
|
return decoded_data if isinstance(decoded_data, list) else []
|
||||||
|
except (requests.RequestException, json.JSONDecodeError) as exc:
|
||||||
|
raise ValueError("Could not get valid anime: ") from exc
|
||||||
|
|
||||||
|
def AniWorld_download_episode(directory, folder, season, episode, key):
|
||||||
|
"""Helper function to download an individual episode."""
|
||||||
|
try:
|
||||||
|
folder_path = os.path.join(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:
|
||||||
|
download_error_logger.error(f"Language not found for anime: {key} season: {season} episode: {episode}")
|
||||||
|
except NoMachingLanguage as ee:
|
||||||
|
download_error_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}")
|
||||||
|
|
||||||
def download(anime: Anime): # pylint: disable=too-many-branches
|
def download(anime: Anime): # pylint: disable=too-many-branches
|
||||||
for episode in anime:
|
for episode in anime:
|
||||||
sanitized_anime_title = ''.join(
|
sanitized_anime_title = ''.join(
|
||||||
@ -43,7 +99,8 @@ def download(anime: Anime): # pylint: disable=too-many-branches
|
|||||||
#"--concurrent-fragments", "4",
|
#"--concurrent-fragments", "4",
|
||||||
"-o", output_path,
|
"-o", output_path,
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"--no-warnings"
|
"--no-warnings",
|
||||||
|
"--progress"
|
||||||
]
|
]
|
||||||
|
|
||||||
if anime.provider in PROVIDER_HEADERS:
|
if anime.provider in PROVIDER_HEADERS:
|
||||||
184
src/Main.py
Normal file
184
src/Main.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import AniWorldLoader
|
||||||
|
import SerieList
|
||||||
|
import SerieScanner
|
||||||
|
|
||||||
|
# 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 SeriesApp:
|
||||||
|
def __init__(self, directory_to_search: str):
|
||||||
|
self.directory_to_search = directory_to_search
|
||||||
|
self.SerieScanner = SerieScanner.SerieScanner(directory_to_search)
|
||||||
|
|
||||||
|
self.List = SerieList.SerieList(self.directory_to_search)
|
||||||
|
print("Please wait while initializing...")
|
||||||
|
self.__InitList__()
|
||||||
|
|
||||||
|
def __InitList__(self):
|
||||||
|
self.series_list = self.List.GetMissingEpisode()
|
||||||
|
|
||||||
|
|
||||||
|
def display_series(self):
|
||||||
|
"""Print all series with assigned numbers."""
|
||||||
|
print("\nCurrent result:")
|
||||||
|
for i, serie in enumerate(self.series_list, 1):
|
||||||
|
print(f"{i}. {serie}")
|
||||||
|
|
||||||
|
def search(self, words :str) -> list:
|
||||||
|
return AniWorldLoader.search_anime(words)
|
||||||
|
|
||||||
|
def get_user_selection(self):
|
||||||
|
"""Handle user input for selecting series."""
|
||||||
|
self.display_series()
|
||||||
|
while True:
|
||||||
|
selection = input(
|
||||||
|
"\nSelect series by number (e.g. '1', '1,2' or 'all') or type 'rescan' to refresh: ").strip().lower()
|
||||||
|
|
||||||
|
if selection == "rescan":
|
||||||
|
self.rescan()
|
||||||
|
return None
|
||||||
|
|
||||||
|
selected_series = []
|
||||||
|
if selection == "all":
|
||||||
|
selected_series = self.series_list
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
indexes = [int(num) - 1 for num in selection.split(",")]
|
||||||
|
selected_series = [self.series_list[i] for i in indexes if 0 <= i < len(self.series_list)]
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid selection. Going back to the result display.")
|
||||||
|
self.display_series()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if selected_series:
|
||||||
|
return selected_series
|
||||||
|
else:
|
||||||
|
print("No valid series selected. Going back to the result display.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def print_progress_bar(self, current, total, length=20):
|
||||||
|
"""Generate progress bar string"""
|
||||||
|
filled_length = int(length * current // total)
|
||||||
|
bar = "@" * filled_length + "-" * (length - filled_length)
|
||||||
|
return f"[{bar}] {current} / {total}"
|
||||||
|
|
||||||
|
def download_series(self, series):
|
||||||
|
"""Simulate the downloading process with a progress bar."""
|
||||||
|
total_downloaded = 0
|
||||||
|
total_episodes = sum(sum(len(ep) for ep in serie.episodeDict.values()) for serie in series)
|
||||||
|
|
||||||
|
for serie in series:
|
||||||
|
serie_episodes = sum(len(ep) for ep in serie.episodeDict.values())
|
||||||
|
downloaded = 0
|
||||||
|
|
||||||
|
print(f"\nStarting download for {serie.folder}...\n")
|
||||||
|
|
||||||
|
for season, episodes in serie.episodeDict.items():
|
||||||
|
for episode in episodes:
|
||||||
|
|
||||||
|
if serie.site == "aniworld.to":
|
||||||
|
AniWorldLoader.AniWorld_download_episode(self.directory_to_search, serie.folder, season, episode, serie.key)
|
||||||
|
|
||||||
|
downloaded += 1
|
||||||
|
total_downloaded += 1
|
||||||
|
progress = self.print_progress_bar(downloaded, serie_episodes)
|
||||||
|
total_progress = self.print_progress_bar(total_downloaded, total_episodes)
|
||||||
|
|
||||||
|
sys.stdout.write(f"\r{serie.name}: {progress} (Total: {total_progress})")
|
||||||
|
sys.stdout.flush()
|
||||||
|
print("\nDownload complete!\n")
|
||||||
|
|
||||||
|
def rescan(self):
|
||||||
|
"""Rescan and refresh the series list."""
|
||||||
|
print("\nRescanning series...\n")
|
||||||
|
self.SerieScanner.Scan()
|
||||||
|
self.__InitList__()
|
||||||
|
|
||||||
|
def search_mode(self):
|
||||||
|
"""Search for a series and allow user to select an option."""
|
||||||
|
search_string = input("Enter search string: ").strip()
|
||||||
|
results = self.search(search_string)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
print("No results found. Returning to start.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\nSearch results:")
|
||||||
|
for i, result in enumerate(results, 1):
|
||||||
|
print(f"{i}. {result}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
selection = input("\nSelect an option by number or type 'start' to return: ").strip().lower()
|
||||||
|
|
||||||
|
if selection == "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = int(selection) - 1
|
||||||
|
if 0 <= index < len(results):
|
||||||
|
chosen_name = results[index]
|
||||||
|
|
||||||
|
self.List.add(AniWorldLoader.CreateSerie(chosen_name))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("Invalid selection. Try again.")
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid input. Try again.")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Main function to run the app."""
|
||||||
|
while True:
|
||||||
|
action = input("\nChoose action ('search' or 'local'): ").strip().lower()
|
||||||
|
|
||||||
|
if action == "search":
|
||||||
|
self.search_mode()
|
||||||
|
elif action == "local":
|
||||||
|
selected_series = self.get_user_selection()
|
||||||
|
if selected_series:
|
||||||
|
self.download_series(selected_series)
|
||||||
|
print("\nProgress: [@@@@@@@@@@@@@@@@@@@@] Complete")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Invalid action. Please enter 'search' or 'local'.")
|
||||||
|
|
||||||
|
# Run the app
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 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")
|
||||||
|
app = SeriesApp(directory_to_search)
|
||||||
|
app.run()
|
||||||
46
src/Serie.py
46
src/Serie.py
@ -1,24 +1,16 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
class Serie:
|
class Serie:
|
||||||
def __init__(self, key: str, name: str, site: str, folder: str):
|
def __init__(self, key: str, name: str, site: str, folder: str, episodeDict: dict[int, list[int]]):
|
||||||
self._key = key
|
self._key = key
|
||||||
self._name = name
|
self._name = name
|
||||||
self._site = site
|
self._site = site
|
||||||
self._folder = folder
|
self._folder = folder
|
||||||
self._episodeDict = dict[int, list[int]]
|
self._episodeDict = episodeDict
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""String representation of Serie object"""
|
"""String representation of Serie object"""
|
||||||
return f"Serie(key='{self.key}', name='{self.name}', site='{self.site}', folder='{self.folder}', episodeDict={self.episodeDict})"
|
return f"Serie(key='{self.key}', name='{self.name}', site='{self.site}', folder='{self.folder}', episodeDict={self.episodeDict})"
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
"""Convert Serie object to dictionary"""
|
|
||||||
return {
|
|
||||||
"key": self.key,
|
|
||||||
"name": self.name,
|
|
||||||
"site": self.site,
|
|
||||||
"folder": self.folder,
|
|
||||||
"episodeDict": {str(k): v for k, v in self._episodeDict.items()} # Convert int keys to str
|
|
||||||
}
|
|
||||||
@property
|
@property
|
||||||
def key(self) -> str:
|
def key(self) -> str:
|
||||||
return self._key
|
return self._key
|
||||||
@ -58,3 +50,33 @@ class Serie:
|
|||||||
@episodeDict.setter
|
@episodeDict.setter
|
||||||
def episodeDict(self, value: dict[int, list[int]]):
|
def episodeDict(self, value: dict[int, list[int]]):
|
||||||
self._episodeDict = value
|
self._episodeDict = value
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert Serie object to dictionary for JSON serialization."""
|
||||||
|
return {
|
||||||
|
"key": self.key,
|
||||||
|
"name": self.name,
|
||||||
|
"site": self.site,
|
||||||
|
"folder": self.folder,
|
||||||
|
"episodeDict": {str(k): list(v) for k, v in self.episodeDict.items()}
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data: dict):
|
||||||
|
"""Create a Serie object from dictionary."""
|
||||||
|
episode_dict = {int(k): v for k, v in data["episodeDict"].items()} # Convert keys to int
|
||||||
|
return Serie(data["key"], data["name"], data["site"], data["folder"], episode_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def save_to_file(self, filename: str):
|
||||||
|
"""Save Serie object to JSON file."""
|
||||||
|
with open(filename, "w") as file:
|
||||||
|
json.dump(self.to_dict(), file, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_from_file(cls, filename: str) -> "Serie":
|
||||||
|
"""Load Serie object from JSON file."""
|
||||||
|
with open(filename, "r") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
return cls.from_dict(data)
|
||||||
45
src/SerieList.py
Normal file
45
src/SerieList.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from Serie import Serie
|
||||||
|
class SerieList:
|
||||||
|
def __init__(self, basePath: str):
|
||||||
|
self.directory = basePath
|
||||||
|
self.folderDict: dict[str, Serie] = {} # Proper initialization
|
||||||
|
self.load_series()
|
||||||
|
|
||||||
|
def add(self, serie:Serie):
|
||||||
|
dataPath = os.path.join(self.directory, serie.folder, "data")
|
||||||
|
animePath = os.path.join(self.directory, serie.folder)
|
||||||
|
os.makedirs(animePath, exist_ok=True)
|
||||||
|
if not os.path.isfile(dataPath):
|
||||||
|
serie.save_to_file(dataPath)
|
||||||
|
self.folderDict[serie.folder] = serie;
|
||||||
|
|
||||||
|
def load_series(self):
|
||||||
|
""" Scan folders and load data files """
|
||||||
|
logging.info(f"Scanning anime folders in: {self.directory}")
|
||||||
|
for anime_folder in os.listdir(self.directory):
|
||||||
|
anime_path = os.path.join(self.directory, anime_folder, "data")
|
||||||
|
if os.path.isfile(anime_path):
|
||||||
|
logging.info(f"Found data folder: {anime_path}")
|
||||||
|
self.load_data(anime_folder, anime_path)
|
||||||
|
else:
|
||||||
|
logging.warning(f"Skipping {anime_folder} - No data folder found")
|
||||||
|
|
||||||
|
def load_data(self, anime_folder, data_path):
|
||||||
|
""" Load pickle files from the data folder """
|
||||||
|
try:
|
||||||
|
self.folderDict[anime_folder] = Serie.load_from_file(data_path)
|
||||||
|
logging.info(f"Successfully loaded {data_path} for {anime_folder}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to load {data_path} in {anime_folder}: {e}")
|
||||||
|
|
||||||
|
def GetMissingEpisode(self):
|
||||||
|
"""Find all series with a non-empty episodeDict"""
|
||||||
|
return [serie for serie in self.folderDict.values() if len(serie.episodeDict) > 0]
|
||||||
|
|
||||||
|
|
||||||
|
#k = AnimeList("\\\\sshfs.r\\ubuntu@192.168.178.43\\media\\serien\\Serien")
|
||||||
|
#bbabab = k.GetMissingEpisode()
|
||||||
|
#print(bbabab)
|
||||||
@ -2,7 +2,6 @@ import os
|
|||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import pickle
|
|
||||||
from Serie import Serie
|
from Serie import Serie
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
@ -11,16 +10,15 @@ from Exceptions import NoKeyFoundException, MatchNotFoundError
|
|||||||
import requests
|
import requests
|
||||||
from aniworld.common import get_season_episode_count
|
from aniworld.common import get_season_episode_count
|
||||||
|
|
||||||
class FolderLookup:
|
class SerieScanner:
|
||||||
def __init__(self, basePath: str):
|
def __init__(self, basePath: str):
|
||||||
self.directory = basePath
|
self.directory = basePath
|
||||||
self.folderDict: dict[str, list[Serie]] = {} # Proper initialization
|
self.folderDict: dict[str, Serie] = {} # Proper initialization
|
||||||
logging.info(f"Initialized Loader with base path: {self.directory}")
|
logging.info(f"Initialized Loader with base path: {self.directory}")
|
||||||
|
|
||||||
self.__init()
|
|
||||||
def is_null_or_whitespace(self, s):
|
def is_null_or_whitespace(self, s):
|
||||||
return s is None or s.strip() == ""
|
return s is None or s.strip() == ""
|
||||||
def __init(self):
|
def Scan(self):
|
||||||
logging.info("Starting process to load missing episodes")
|
logging.info("Starting process to load missing episodes")
|
||||||
result = self.__find_mp4_files()
|
result = self.__find_mp4_files()
|
||||||
|
|
||||||
@ -28,16 +26,15 @@ class FolderLookup:
|
|||||||
try:
|
try:
|
||||||
serie = self.__ReadDataFromFile(folder)
|
serie = self.__ReadDataFromFile(folder)
|
||||||
if (serie != None and not self.is_null_or_whitespace(serie.key)):
|
if (serie != None and not self.is_null_or_whitespace(serie.key)):
|
||||||
continue
|
|
||||||
missings, site = self.__GetMissingEpisodesAndSeason(serie.key, mp4_files)
|
missings, site = self.__GetMissingEpisodesAndSeason(serie.key, mp4_files)
|
||||||
serie.episodeDict = missings
|
serie.episodeDict = missings
|
||||||
self.__SaveData(serie, folder)
|
serie.save_to_file(os.path.join(os.path.join(self.directory, folder), 'data'))
|
||||||
if folder not in self.folderDict:
|
if folder not in self.folderDict:
|
||||||
self.folderDict[folder] = []
|
self.folderDict[folder] = []
|
||||||
self.folderDict[folder].append(serie)
|
self.folderDict[folder] = serie
|
||||||
noKeyFound_logger.info(f"Saved Serie: '{str(serie)}'")
|
noKeyFound_logger.info(f"Saved Serie: '{str(serie)}'")
|
||||||
except NoKeyFoundException as nkfe:
|
except NoKeyFoundException as nkfe:
|
||||||
noKeyFound_logger.error(f"Error processing folder '{folder}': {nkfe}")
|
NoKeyFoundException.error(f"Error processing folder '{folder}': {nkfe}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_logger.error(f"Folder: '{folder}' - Unexpected error processing folder '{folder}': {e} \n {traceback.format_exc()}")
|
error_logger.error(f"Folder: '{folder}' - Unexpected error processing folder '{folder}': {e} \n {traceback.format_exc()}")
|
||||||
continue
|
continue
|
||||||
@ -45,35 +42,17 @@ class FolderLookup:
|
|||||||
|
|
||||||
def __find_mp4_files(self):
|
def __find_mp4_files(self):
|
||||||
logging.info("Scanning for .mp4 files")
|
logging.info("Scanning for .mp4 files")
|
||||||
|
for anime_name in os.listdir(self.directory):
|
||||||
for root_folder_name in os.listdir(self.directory):
|
anime_path = os.path.join(self.directory, anime_name)
|
||||||
|
if os.path.isdir(anime_path):
|
||||||
folder_data = defaultdict(list) # Dictionary to store MP4 files per folder
|
mp4_files = []
|
||||||
folder = os.path.join(self.directory, root_folder_name)
|
has_files = False
|
||||||
|
for root, _, files in os.walk(anime_path):
|
||||||
logging.info(f"Processing folder: {root_folder_name}")
|
for file in files:
|
||||||
|
if file.endswith(".mp4"):
|
||||||
# First pass: Scan all folders and collect MP4 file data
|
mp4_files.append(os.path.join(root, file))
|
||||||
for root, dirs, files in os.walk(folder):
|
has_files = True
|
||||||
mp4_files = [file for file in files if file.endswith('.mp4')]
|
yield anime_name, mp4_files if has_files else []
|
||||||
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):
|
def __remove_year(self, input_string: str):
|
||||||
cleaned_string = re.sub(r'\(\d{4}\)', '', input_string).strip()
|
cleaned_string = re.sub(r'\(\d{4}\)', '', input_string).strip()
|
||||||
@ -88,25 +67,17 @@ class FolderLookup:
|
|||||||
|
|
||||||
if os.path.exists(key_file):
|
if os.path.exists(key_file):
|
||||||
with open(key_file, 'r') as file:
|
with open(key_file, 'r') as file:
|
||||||
logging.info(f"Key found for folder '{folder_name}': {key}")
|
|
||||||
key = file.read().strip()
|
key = file.read().strip()
|
||||||
return Serie(key, "", "aniworld.to" ,folder_name)
|
logging.info(f"Key found for folder '{folder_name}': {key}")
|
||||||
|
return Serie(key, "", "aniworld.to", folder_name, dict())
|
||||||
|
|
||||||
if os.path.exists(serie_file):
|
if os.path.exists(serie_file):
|
||||||
with open(serie_file, "rb") as file:
|
with open(serie_file, "rb") as file:
|
||||||
logging.info(f"load serie_file from '{folder_name}': {serie_file}")
|
logging.info(f"load serie_file from '{folder_name}': {serie_file}")
|
||||||
return pickle.load(file)
|
return Serie.load_from_file(serie_file)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __SaveData(self, serie: Serie, folder_name: str):
|
|
||||||
"""Saves a Serie object to a file using JSON."""
|
|
||||||
|
|
||||||
folder_path = os.path.join(self.directory, folder_name)
|
|
||||||
serie_file = os.path.join(folder_path, 'data')
|
|
||||||
with open(serie_file, "w", encoding="utf-8") as file:
|
|
||||||
json.dump(serie.to_dict(), file, indent=4)
|
|
||||||
|
|
||||||
|
|
||||||
def __GetEpisodeAndSeason(self, filename: str):
|
def __GetEpisodeAndSeason(self, filename: str):
|
||||||
pattern = r'S(\d+)E(\d+)'
|
pattern = r'S(\d+)E(\d+)'
|
||||||
@ -143,8 +114,8 @@ class FolderLookup:
|
|||||||
missing_episodes = [ep for ep in range(1, expected_count + 1) if ep not in existing_episodes]
|
missing_episodes = [ep for ep in range(1, expected_count + 1) if ep not in existing_episodes]
|
||||||
|
|
||||||
if missing_episodes:
|
if missing_episodes:
|
||||||
episodes_dict[season] = [missing_episodes]
|
episodes_dict[season] = missing_episodes
|
||||||
|
|
||||||
return episodes_dict, "aniworld.to"
|
return episodes_dict, "aniworld.to"
|
||||||
|
|
||||||
gg = FolderLookup("\\\\sshfs.r\\ubuntu@192.168.178.43\\media\\serien\\Serien")
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
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 ConsoleLoader:
|
|
||||||
def __init__(self, basePath: str):
|
|
||||||
self.directory = basePath
|
|
||||||
logging.info(f"Initialized Loader with base path: {self.directory}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
187
src/main.py
187
src/main.py
@ -1,187 +0,0 @@
|
|||||||
import os
|
|
||||||
import traceback
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
from collections import defaultdict
|
|
||||||
from aniworld.models import Anime, Episode
|
|
||||||
from aniworld.common import get_season_episode_count
|
|
||||||
from aniworld.search import search_anime
|
|
||||||
from src.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}")
|
|
||||||
|
|
||||||
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:
|
|
||||||
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()
|
|
||||||
Loading…
x
Reference in New Issue
Block a user