Enhance RenameGui with AniDB data fetching and improve UI layout
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
|
||||
|
||||
253
RenameGui.py
253
RenameGui.py
@@ -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()
|
||||
Reference in New Issue
Block a user