Enhance RenameGui with AniDB data fetching and improve UI layout

This commit is contained in:
2026-02-02 16:21:55 +07:00
parent 3c45932902
commit 663d1b9959
2 changed files with 183 additions and 76 deletions

6
.gitignore vendored
View File

@@ -174,3 +174,9 @@ cython_debug/
# Built Visual Studio Code Extensions
*.vsix
test.py
.vscode/launch.json
av1_quality_report.csv
av1_quality_report.csv.bak
output.mp3
av1_quality_report.csv.cleaned.csv

View File

@@ -1,71 +1,164 @@
import tkinter as tk
from tkinter import filedialog, scrolledtext, ttk
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.split('\n')
formatted_dict = {}
for line in lines:
parts = line.split('\t')
if len(parts) >= 2:
key = int(parts[0])
value = parts[1].replace("`", "'")
formatted_dict[key] = value
return formatted_dict
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):
# Check if the filename contains any invalid characters
invalid_chars = r'[<>:"/\\|?*]'
if re.search(invalid_chars, filename):
return False
# Check if the filename ends with a space or a period
if filename.endswith(' ') or filename.endswith('.'):
return False
# Check if the filename is a reserved Windows name
reserved_names = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']
if filename.upper() in reserved_names:
return False
return True
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()
if is_valid_windows_filename(title):
override_entry.config(style="TEntry")
override_label.config(text="Override Show Title:", foreground="black")
else:
override_entry.config(style="Invalid.TEntry")
override_label.config(text="Invalid Windows filename!", foreground="red")
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))
pattern = regex_pattern
for filename in os.listdir(folder_path):
full_path = os.path.join(folder_path, filename)
match = re.match(pattern, 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()
folder_entry.delete(0, tk.END)
folder_entry.insert(0, folder_path)
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()
@@ -76,14 +169,16 @@ def start_renaming():
episode_group = int(episode_group_entry.get())
if not folder_path:
tk.messagebox.showerror("Error", "Please select a folder.")
messagebox.showerror("Error", "Please select a folder.")
return
if override_title and not is_valid_windows_filename(override_title):
tk.messagebox.showerror("Error", "The override title is not a valid Windows filename.")
messagebox.showerror("Error", "The override title is not a valid Windows filename.")
return
# Redirect print output to the log
save_regex_to_history(regex_pattern)
regex_entry['values'] = load_regex_history()
old_stdout = sys.stdout
sys.stdout = io.StringIO()
@@ -95,70 +190,76 @@ def start_renaming():
log_text.delete("1.0", tk.END)
log_text.insert(tk.END, log_output)
# Create the main window
# --- GUI ---
root = tk.Tk()
root.title("File Renamer")
root.geometry("850x650") # Slightly larger to accommodate new button
# Create a custom style for invalid input
style = ttk.Style()
style.configure("Invalid.TEntry", fieldbackground="pink")
# Create a notebook (tabbed interface)
notebook = ttk.Notebook(root)
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Create the main tab
main_frame = ttk.Frame(notebook, padding="10")
notebook.add(main_frame, text="Main")
# Create the options tab
options_frame = ttk.Frame(notebook, padding="10")
notebook.add(main_frame, text="Main")
notebook.add(options_frame, text="Options")
# Main tab widgets
ttk.Label(main_frame, text="Folder Path:").grid(column=0, row=0, sticky=tk.W)
folder_entry = ttk.Entry(main_frame, width=50)
folder_entry.grid(column=1, row=0, sticky=(tk.W, tk.E))
ttk.Button(main_frame, text="Browse", command=select_folder).grid(column=2, row=0, sticky=tk.W)
# 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)
ttk.Label(main_frame, text="Regex Pattern:").grid(column=0, row=1, sticky=tk.W)
regex_entry = ttk.Entry(main_frame, width=50)
regex_entry.grid(column=1, row=1, sticky=(tk.W, tk.E))
regex_entry.insert(0, r'\[SubsPlease\] (.+?) - (\d{2}) \(1080p\) \[\w+\]\.mkv')
# 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="Episode Titles:").grid(column=0, row=2, sticky=tk.W)
titles_text = scrolledtext.ScrolledText(main_frame, width=60, height=10)
titles_text.grid(column=0, row=3, columnspan=3, sticky=(tk.W, tk.E))
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)
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.Button(main_frame, text="Rename Files", command=start_renaming).grid(column=1, row=4, sticky=tk.E)
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)
ttk.Label(main_frame, text="Log:").grid(column=0, row=5, sticky=tk.W)
log_text = scrolledtext.ScrolledText(main_frame, width=60, height=10, state='normal')
log_text.grid(column=0, row=6, columnspan=3, sticky=(tk.W, tk.E))
# Options tab widgets
ttk.Label(options_frame, text="Episode Number Offset:").grid(column=0, row=0, sticky=tk.W)
# 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)
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)
override_entry = ttk.Entry(options_frame, width=50)
override_entry.grid(column=1, row=1, sticky=(tk.W, tk.E))
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)
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)
episode_group_entry.grid(column=1, row=2, sticky=tk.W, padx=5, pady=2)
episode_group_entry.insert(0, "2")
# Configure grid expansion
main_frame.columnconfigure(1, weight=1)
# 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)
# Start the GUI event loop
# 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()