Files
Media_Scripts/RenameGui.py

265 lines
11 KiB
Python

import tkinter as tk
from tkinter import filedialog, scrolledtext, ttk, messagebox
import os
import re
import io
import sys
import requests # Add this import
import xml.etree.ElementTree as ET # Add this import
HISTORY_FILE = "regex_history.txt"
def fetch_anidb_data():
"""Fetch episode data from AniDB based on folder name pattern {anidbN-X}"""
folder_path = folder_entry.get()
if not folder_path:
messagebox.showerror("Error", "Please select a folder first.")
return
# Get just the folder name from the path
folder_name = os.path.basename(folder_path)
# Extract anidb ID using regex (handles patterns like {anidb4-18874})
match = re.search(r'\{anidb\d+-(\d+)\}', folder_name)
if not match:
messagebox.showerror("Error", "Could not find valid AniDB ID in folder name.\nFolder name should contain pattern like: {anidb4-18874}")
return
aid = match.group(1)
print(f"Found AniDB ID: {aid}")
# Fetch XML from AniDB API
url = f"http://api.anidb.net:9001/httpapi?request=anime&client=testdesktop&clientver=1&protover=1&aid={aid}"
try:
print(f"Fetching data from: {url}")
response = requests.get(url, timeout=10)
response.raise_for_status() # Raise exception for HTTP errors
except requests.exceptions.RequestException as e:
messagebox.showerror("Network Error", f"Failed to fetch AniDB data:\n{str(e)}")
return
# Parse XML and extract episodes
try:
root = ET.fromstring(response.content)
except ET.ParseError as e:
messagebox.showerror("XML Error", f"Failed to parse AniDB response:\n{str(e)}")
return
# Extract regular episodes (type="1") with their numbers and titles
episodes = []
for episode in root.findall('.//episode'):
epno_elem = episode.find('epno')
if epno_elem is not None and epno_elem.get('type') == '1':
try:
epno = int(epno_elem.text)
except (ValueError, TypeError):
continue
# Get all title elements for this episode
title_elements = episode.findall('title')
# Create a dictionary of titles by language
titles_by_lang = {}
for title_elem in title_elements:
lang = title_elem.get('{http://www.w3.org/XML/1998/namespace}lang')
if lang and title_elem.text:
titles_by_lang[lang] = title_elem.text
# Try to get English title first, then romanized (x-jat), then any available
title = None
for lang in ['en', 'x-jat', 'ja']:
if lang in titles_by_lang:
title = titles_by_lang[lang]
break
# Use a fallback if no title found
if title is None and title_elements:
# Get the first non-empty title
for title_elem in title_elements:
if title_elem.text and title_elem.text.strip():
title = title_elem.text
break
if title is None:
title = f"Episode {epno}"
episodes.append((epno, title))
if not episodes:
messagebox.showwarning("No Episodes Found", "No regular episodes were found in the AniDB response.")
return
# Sort episodes by number
episodes.sort(key=lambda x: x[0])
# Format for the titles text area (tab-separated)
formatted_titles = "\n".join([f"{epno}\t{title}" for epno, title in episodes])
# Update the titles text area
titles_text.delete("1.0", tk.END)
titles_text.insert(tk.END, formatted_titles)
print(f"Successfully loaded {len(episodes)} episodes from AniDB")
def format_titles(titles_data):
lines = titles_data.strip().split('\n')
return {
int(parts[0]): parts[1].replace("`", "'")
for line in lines if (parts := line.split('\t')) and len(parts) >= 2
}
def is_valid_windows_filename(filename):
invalid_chars = r'[<>:"/\\|?*]'
if re.search(invalid_chars, filename): return False
if filename.endswith(' ') or filename.endswith('.'): return False
reserved = {'CON', 'PRN', 'AUX', 'NUL'} | {f'COM{i}' for i in range(1, 10)} | {f'LPT{i}' for i in range(1, 10)}
return filename.upper() not in reserved
def validate_override_title(event=None):
title = override_entry.get()
valid = is_valid_windows_filename(title)
override_entry.config(style="TEntry" if valid else "Invalid.TEntry")
override_label.config(text="Override Show Title:" if valid else "Invalid Windows filename!", foreground="black" if valid else "red")
def rename_files(folder_path, dry_run, regex_pattern, episode_offset, override_title, episode_group):
episode_titles_dic = format_titles(titles_text.get("1.0", tk.END))
for filename in os.listdir(folder_path):
full_path = os.path.join(folder_path, filename)
match = re.match(regex_pattern, filename)
if match:
series_title = override_title if override_title else match.group(1)
episode_number = int(match.group(episode_group)) + episode_offset
episode_title = episode_titles_dic.get(episode_number, "Unknown Title")
episode_title = re.sub(r'[\\/:*?"<>|]', '_', episode_title)
new_filename = f"{series_title} - EP{episode_number:02d} - {episode_title}.mkv"
new_full_path = os.path.join(folder_path, new_filename)
if not dry_run:
os.rename(full_path, new_full_path)
print(f"Renamed '{filename}' to '{new_filename}'")
def select_folder():
folder_path = filedialog.askdirectory()
if folder_path:
folder_entry.delete(0, tk.END)
folder_entry.insert(0, folder_path)
def save_regex_to_history(pattern):
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
history = [line.strip() for line in f if line.strip()]
else:
history = []
if pattern not in history:
history.insert(0, pattern)
with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
f.write('\n'.join(history[:20])) # limit history
def load_regex_history():
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
return [line.strip() for line in f if line.strip()]
return []
def start_renaming():
folder_path = folder_entry.get()
dry_run = dry_run_var.get()
regex_pattern = regex_entry.get()
episode_offset = int(offset_entry.get())
override_title = override_entry.get()
episode_group = int(episode_group_entry.get())
if not folder_path:
messagebox.showerror("Error", "Please select a folder.")
return
if override_title and not is_valid_windows_filename(override_title):
messagebox.showerror("Error", "The override title is not a valid Windows filename.")
return
save_regex_to_history(regex_pattern)
regex_entry['values'] = load_regex_history()
old_stdout = sys.stdout
sys.stdout = io.StringIO()
rename_files(folder_path, dry_run, regex_pattern, episode_offset, override_title, episode_group)
log_output = sys.stdout.getvalue()
sys.stdout = old_stdout
log_text.delete("1.0", tk.END)
log_text.insert(tk.END, log_output)
# --- GUI ---
root = tk.Tk()
root.title("File Renamer")
root.geometry("850x650") # Slightly larger to accommodate new button
style = ttk.Style()
style.configure("Invalid.TEntry", fieldbackground="pink")
notebook = ttk.Notebook(root)
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
main_frame = ttk.Frame(notebook, padding="10")
options_frame = ttk.Frame(notebook, padding="10")
notebook.add(main_frame, text="Main")
notebook.add(options_frame, text="Options")
# Main Tab
ttk.Label(main_frame, text="Folder Path:").grid(column=0, row=0, sticky=tk.W, pady=2)
folder_entry = ttk.Entry(main_frame)
folder_entry.grid(column=1, row=0, sticky="ew", padx=5, pady=2)
ttk.Button(main_frame, text="Browse", command=select_folder).grid(column=2, row=0, sticky=tk.W, padx=5, pady=2)
# Add the AniDB fetch button
ttk.Button(main_frame, text="Fetch from AniDB", command=fetch_anidb_data).grid(column=3, row=0, sticky=tk.W, padx=5, pady=2)
ttk.Label(main_frame, text="Regex Pattern:").grid(column=0, row=1, sticky=tk.W, pady=2)
regex_entry = ttk.Combobox(main_frame, values=load_regex_history())
regex_entry.grid(column=1, row=1, columnspan=3, sticky="ew", padx=5, pady=2)
regex_entry.set(r'\[SubsPlease\] (.+?) - (\d{2}) \(1080p\) \[\w+\]\.mkv')
ttk.Label(main_frame, text="Episode Titles:").grid(column=0, row=2, sticky=tk.W, pady=2)
titles_text = scrolledtext.ScrolledText(main_frame, height=8)
titles_text.grid(column=0, row=3, columnspan=4, sticky="nsew", padx=5, pady=2)
dry_run_var = tk.BooleanVar(value=True)
ttk.Checkbutton(main_frame, text="Dry Run", variable=dry_run_var).grid(column=0, row=4, sticky=tk.W, pady=2)
ttk.Button(main_frame, text="Rename Files", command=start_renaming).grid(column=3, row=4, sticky=tk.E, pady=2)
ttk.Label(main_frame, text="Log:").grid(column=0, row=5, sticky=tk.W, pady=2)
log_text = scrolledtext.ScrolledText(main_frame)
log_text.grid(column=0, row=6, columnspan=4, sticky="nsew", padx=5, pady=2)
# Options Tab
ttk.Label(options_frame, text="Episode Number Offset:").grid(column=0, row=0, sticky=tk.W, pady=2)
offset_entry = ttk.Entry(options_frame, width=10)
offset_entry.grid(column=1, row=0, sticky=tk.W, padx=5, pady=2)
offset_entry.insert(0, "0")
override_label = ttk.Label(options_frame, text="Override Show Title:")
override_label.grid(column=0, row=1, sticky=tk.W, pady=2)
override_entry = ttk.Entry(options_frame)
override_entry.grid(column=1, row=1, sticky="ew", padx=5, pady=2)
override_entry.bind("<KeyRelease>", validate_override_title)
ttk.Label(options_frame, text="Episode Number Regex Group:").grid(column=0, row=2, sticky=tk.W, pady=2)
episode_group_entry = ttk.Entry(options_frame, width=10)
episode_group_entry.grid(column=1, row=2, sticky=tk.W, padx=5, pady=2)
episode_group_entry.insert(0, "2")
# Expandable configuration
for i in range(4): # Updated to include the new column
main_frame.columnconfigure(i, weight=1 if i == 1 else 0)
main_frame.rowconfigure(3, weight=1)
main_frame.rowconfigure(6, weight=2)
options_frame.columnconfigure(1, weight=1)
# Add instructions label
instructions = ttk.Label(main_frame, text="Note: Folder name must contain pattern {anidb4-12345} to fetch from AniDB",
font=("Arial", 8), foreground="gray")
instructions.grid(column=0, row=7, columnspan=4, sticky=tk.W, pady=(5,0))
root.mainloop()