Guest User

Untitled

a guest
Oct 15th, 2025
24
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.80 KB | None | 0 0
  1. import os
  2. import requests
  3. import json
  4. from pathlib import Path
  5. import logging
  6. import pickle
  7. from requests.adapters import HTTPAdapter
  8. from urllib3.util.retry import Retry
  9. import time
  10.  
  11. # Base directories
  12. source_dir = "/mnt/data/Anime"
  13. target_dir = "/mnt/data/anime-jellyfin"
  14.  
  15. # Configure logging
  16. log_dir = target_dir
  17. logging.basicConfig(
  18. filename=os.path.join(log_dir, "symlink_creation.log"),
  19. level=logging.INFO,
  20. format="%(asctime)s - %(levelname)s - %(message)s"
  21. )
  22.  
  23. # Cache file to track processed directories and files
  24. CACHE_FILE = os.path.join(target_dir, "processed_dirs.pkl")
  25.  
  26. # Ollama API endpoint
  27. OLLAMA_URL = "http://localhost:11434/api/generate"
  28.  
  29. # Supported media file extensions
  30. MEDIA_EXTENSIONS = {'.mkv', '.mp4', '.avi', '.ts', '.m4v', '.mpeg', '.mpg', '.srt', '.ass'}
  31.  
  32. # Function to create a session with retry logic
  33. def create_requests_session():
  34. session = requests.Session()
  35. retries = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])
  36. session.mount("http://", HTTPAdapter(max_retries=retries))
  37. return session
  38.  
  39. # Function to check if Ollama is reachable
  40. def check_ollama_availability():
  41. session = create_requests_session()
  42. try:
  43. response = session.post(OLLAMA_URL, json={
  44. "model": "gemma3-27b-qat",
  45. "prompt": "Test connectivity",
  46. "stream": False
  47. }, timeout=30)
  48. response.raise_for_status()
  49. logging.info("Successfully connected to Ollama.")
  50. print("Successfully connected to Ollama.")
  51. return True
  52. except Exception as e:
  53. logging.error(f"Cannot connect to Ollama at {OLLAMA_URL}: {e}")
  54. print(f"Error: Cannot connect to Ollama at {OLLAMA_URL}: {e}")
  55. return False
  56.  
  57. # Function to load processed directories from cache
  58. def load_processed_dirs():
  59. if os.path.exists(CACHE_FILE):
  60. with open(CACHE_FILE, "rb") as f:
  61. return pickle.load(f)
  62. return {}
  63.  
  64. # Function to save processed directories to cache
  65. def save_processed_dirs(processed_dirs):
  66. with open(CACHE_FILE, "wb") as f:
  67. pickle.dump(processed_dirs, f)
  68.  
  69. # Function to clean up broken symlinks and empty directories
  70. def cleanup_broken_symlinks_and_empty_dirs(target_dir):
  71. broken_symlinks = 0
  72. empty_dirs_removed = 0
  73. logging.info("Starting cleanup of broken symlinks and empty directories...")
  74. print("Starting cleanup of broken symlinks and empty directories...")
  75.  
  76. for root, dirs, files in os.walk(target_dir, topdown=False):
  77. if root.startswith(log_dir):
  78. continue
  79. for name in files:
  80. path = os.path.join(root, name)
  81. if os.path.islink(path) and not os.path.exists(path):
  82. try:
  83. os.remove(path)
  84. logging.info(f"Removed broken symlink: {path}")
  85. print(f"Removed broken symlink: {path}")
  86. broken_symlinks += 1
  87. except OSError as e:
  88. logging.error(f"Error removing broken symlink {path}: {e}")
  89. print(f"Error removing broken symlink {path}: {e}")
  90. for name in dirs:
  91. path = os.path.join(root, name)
  92. if not os.path.islink(path) and not os.listdir(path):
  93. try:
  94. os.rmdir(path)
  95. logging.info(f"Removed empty directory: {path}")
  96. print(f"Removed empty directory: {path}")
  97. empty_dirs_removed += 1
  98. except OSError as e:
  99. logging.error(f"Error removing empty directory {path}: {e}")
  100. print(f"Error removing empty directory {path}: {e}")
  101.  
  102. logging.info(f"Cleanup completed. Removed {broken_symlinks} broken symlinks, {empty_dirs_removed} empty directories.")
  103. print(f"Cleanup completed. Removed {broken_symlinks} broken symlinks, {empty_dirs_removed} empty directories.")
  104. return broken_symlinks, empty_dirs_removed
  105.  
  106. # Function to query Ollama for parsing directory names
  107. def parse_directory_name(dir_name):
  108. session = create_requests_session()
  109. prompt = f"""
  110. You are an expert at parsing anime directory names. Given the directory name "{dir_name}", extract:
  111. 1. The show title (the main name of the show).
  112. 2. The season number (e.g., 1, 2, 3, etc.). If no season is specified, assume season 1.
  113. 3. Any extra information like "(Dub)" or story arc names (e.g., "Shoal of Time").
  114.  
  115. Return the result as a JSON object with keys: "show_name", "season", "extras".
  116. Examples:
  117. - "100-man no Inochi no Ue ni Ore wa Tatteiru 2nd Season" -> {{"show_name": "100-man no Inochi no Ue ni Ore wa Tatteiru", "season": 2, "extras": ""}}
  118. - "91 Days - Shoal of TimeAll Our YesterdaysTomorrow and Tomorrow" -> {{"show_name": "91 Days", "season": 1, "extras": "Shoal of TimeAll Our YesterdaysTomorrow and Tomorrow"}}
  119. - "Accel World" -> {{"show_name": "Accel World", "season": 1, "extras": ""}}
  120. - "3-gatsu no Lion (Dub)" -> {{"show_name": "3-gatsu no Lion", "season": 1, "extras": "(Dub)"}}
  121. - ".hackSign" -> {{"show_name": ".hackSign", "season": 1, "extras": ""}}
  122. - "Shuffle!" -> {{"show_name": "Shuffle", "season": 1, "extras": ""}}
  123. """
  124.  
  125. max_retries = 3
  126. for attempt in range(max_retries):
  127. try:
  128. response = session.post(OLLAMA_URL, json={
  129. "model": "gemma3-27b-qat",
  130. "prompt": prompt,
  131. "format": "json",
  132. "stream": False
  133. }, timeout=30)
  134. response.raise_for_status()
  135. result = json.loads(response.json().get("response", "{}"))
  136. show_name = result.get("show_name", dir_name)
  137. season = result.get("season", 1)
  138. extras = result.get("extras", "")
  139.  
  140. try:
  141. season = int(season)
  142. except ValueError:
  143. logging.warning(f"Non-numeric season '{season}' for {dir_name}. Falling back to show_name='{dir_name}', season=1, extras=''")
  144. print(f"Warning: Non-numeric season '{season}' for {dir_name}. Falling back to show_name='{dir_name}', season=1, extras=''")
  145. show_name, season, extras = dir_name, 1, ""
  146.  
  147. return show_name, season, extras
  148. except requests.exceptions.HTTPError as e:
  149. logging.error(f"Attempt {attempt + 1}/{max_retries} failed parsing {dir_name}: {e}. Response: {response.text}")
  150. print(f"Attempt {attempt + 1}/{max_retries} failed parsing {dir_name}: {e}. Response: {response.text}")
  151. if attempt < max_retries - 1:
  152. time.sleep(2)
  153. continue
  154. logging.error(f"Failed to parse {dir_name} after {max_retries} attempts. Exiting.")
  155. print(f"Error: Failed to parse {dir_name} after {max_retries} attempts. Exiting.")
  156. exit(1)
  157. except Exception as e:
  158. logging.error(f"Attempt {attempt + 1}/{max_retries} failed parsing {dir_name}: {e}")
  159. print(f"Attempt {attempt + 1}/{max_retries} failed parsing {dir_name}: {e}")
  160. if attempt < max_retries - 1:
  161. time.sleep(2)
  162. continue
  163. logging.error(f"Failed to parse {dir_name} after {max_retries} attempts. Exiting.")
  164. print(f"Error: Failed to parse {dir_name} after {max_retries} attempts. Exiting.")
  165. exit(1)
  166.  
  167. # Clean up broken symlinks and empty directories
  168. broken_symlinks_removed, empty_dirs_removed = cleanup_broken_symlinks_and_empty_dirs(target_dir)
  169.  
  170. # Check Ollama availability
  171. if not check_ollama_availability():
  172. logging.error("Aborting hardlink creation due to Ollama being unreachable.")
  173. print("Error: Aborting hardlink creation due to Ollama being unreachable.")
  174. exit(1)
  175.  
  176. # Load processed directories
  177. processed_dirs = load_processed_dirs()
  178.  
  179. # Get total directories for progress
  180. dir_list = [d for d in os.listdir(source_dir) if os.path.isdir(os.path.join(source_dir, d))]
  181. total_dirs = len(dir_list)
  182. dirs_processed = 0
  183.  
  184. # Walk through source directory
  185. new_dirs_processed = 0
  186. new_files_linked = 0
  187. for dir_name in dir_list:
  188. source_path = os.path.join(source_dir, dir_name)
  189. dirs_processed += 1
  190. progress = (dirs_processed / total_dirs) * 100
  191. logging.info(f"Processing {dir_name} ({dirs_processed}/{total_dirs}, {progress:.1f}%)")
  192. print(f"Processing {dir_name} ({dirs_processed}/{total_dirs}, {progress:.1f}%)")
  193.  
  194. if os.path.isdir(source_path):
  195. # Check if directory is in cache
  196. if dir_name in processed_dirs:
  197. show_name = processed_dirs[dir_name]["show_name"]
  198. season = processed_dirs[dir_name]["season"]
  199. extras = processed_dirs[dir_name]["extras"]
  200. cached_files = processed_dirs[dir_name]["files"]
  201. else:
  202. # Parse directory name using Ollama
  203. show_name, season, extras = parse_directory_name(dir_name)
  204. cached_files = set()
  205. processed_dirs[dir_name] = {
  206. "show_name": show_name,
  207. "season": season,
  208. "extras": extras,
  209. "files": cached_files
  210. }
  211. new_dirs_processed += 1
  212.  
  213. # Sanitize show name for filesystem compatibility
  214. show_name = show_name.replace("/", "-").replace(":", "-")
  215.  
  216. # Handle dubbed versions
  217. jellyfin_show_name = show_name
  218. if "(Dub)" in extras:
  219. jellyfin_show_name += " (Dub)"
  220.  
  221. # Create Jellyfin-compatible directory structure
  222. jellyfin_show_path = os.path.join(target_dir, jellyfin_show_name, f"Season{season}")
  223. os.makedirs(jellyfin_show_path, exist_ok=True)
  224.  
  225. # Create hard links for new media files
  226. for file_name in os.listdir(source_path):
  227. if os.path.splitext(file_name)[1].lower() in MEDIA_EXTENSIONS and file_name not in cached_files:
  228. source_file = os.path.join(source_path, file_name)
  229. if os.path.isfile(source_file):
  230. hardlink_path = os.path.join(jellyfin_show_path, file_name)
  231. try:
  232. # Check if hardlink already exists
  233. if os.path.exists(hardlink_path):
  234. logging.info(f"Hardlink exists: {hardlink_path}")
  235. print(f"Hardlink exists: {hardlink_path}")
  236. processed_dirs[dir_name]["files"].add(file_name)
  237. continue
  238. # Create hard link
  239. os.link(source_file, hardlink_path)
  240. logging.info(f"Created hardlink: {hardlink_path} -> {source_file}")
  241. print(f"Created hardlink: {hardlink_path} -> {source_file}")
  242. new_files_linked += 1
  243. processed_dirs[dir_name]["files"].add(file_name)
  244. except OSError as e:
  245. logging.error(f"Error creating hardlink for {file_name} in {dir_name}: {e}")
  246. print(f"Error creating hardlink for {file_name} in {dir_name}: {e}")
  247. except FileNotFoundError as e:
  248. logging.error(f"Target file not found for {file_name} in {dir_name}: {e}")
  249. print(f"Target file not found for {file_name} in {dir_name}: {e}")
  250.  
  251. # Save processed directories
  252. save_processed_dirs(processed_dirs)
  253.  
  254. logging.info(f"Hardlink creation completed! Processed {new_dirs_processed} new directories, linked {new_files_linked} new files. Removed {broken_symlinks_removed} broken symlinks, {empty_dirs_removed} empty directories.")
  255. print(f"Hardlink creation completed! Processed {new_dirs_processed} new directories, linked {new_files_linked} new files. Removed {broken_symlinks_removed} broken symlinks, {empty_dirs_removed} empty directories.")
Advertisement
Add Comment
Please, Sign In to add comment