USER
Add th ability to select videos also along side the ability for folders and have the videos process like they are in separate folders but they are not to be able to do the processing that the folder way does here is code import ttkbootstrap as tb
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
from tkinter import filedialog
import os
import subprocess
import threading
import concurrent.futures
import queue
import psutil
import sys
# Constants for the application
APP_NAME = "DeepAscension Video2Image+"
DEFAULT_MAX_CONCURRENT_TASKS = 4
DISK_CHECK_INTERVAL = 5000 # in milliseconds
def select_input_folders():
while True:
folder = filedialog.askdirectory(title="Select Input Folder (Cancel to finish)")
if folder:
if folder not in input_folders:
input_folders.append(folder)
tree.insert('', 'end', iid=folder, values=(folder, "Pending"))
else:
Messagebox.show_warning("Duplicate Folder", f"The folder '{folder}' is already selected.")
else:
break
def remove_selected_folders():
selected_items = tree.selection()
for item in selected_items:
input_folders.remove(item)
tree.delete(item)
update_remove_button()
def select_output_folder():
folder = filedialog.askdirectory(title="Select Output Folder")
if folder:
output_folder_var.set(folder)
output_entry.config(state='normal')
output_entry.delete(0, tb.END)
output_entry.insert(0, folder)
output_entry.config(state='readonly')
def sanitize_folder_name(name):
"""Sanitize the folder name to remove or replace problematic characters."""
return "".join(c if c.isalnum() or c in (' ', '_', '-') else "_" for c in name).replace(" ", "_")
def process_videos_thread():
"""Run the video processing in a separate thread."""
threading.Thread(target=process_videos, daemon=True).start()
def process_videos():
global is_paused
output_folder = output_folder_var.get()
if not input_folders:
Messagebox.show_error("Error", "Please select at least one input folder.")
return
if not output_folder:
Messagebox.show_error("Error", "Please select an output folder.")
return
# Ensure the output directory exists
os.makedirs(output_folder, exist_ok=True)
# Disable the process button and folder selection buttons to prevent multiple clicks
set_buttons_state(DISABLED)
# Initialize overall progress bar
progress_bar['maximum'] = len(input_folders)
progress_bar['value'] = 0
status_label.config(text="Starting processing...")
# Queue to receive progress updates
progress_queue = queue.Queue()
# List to collect errors
errors = []
# Create a copy of input_folders to iterate over
tasks_queue = queue.Queue()
for folder in input_folders:
tasks_queue.put(folder)
# Determine optimal number of workers based on CPU cores
max_workers = min(os.cpu_count() * 2 or DEFAULT_MAX_CONCURRENT_TASKS, 32)
def worker(folder):
if is_paused:
# Wait until resumed
while is_paused:
if stop_all_event.is_set():
return
threading.Event().wait(0.1)
# Update Treeview status to "Processing"
progress_queue.put(("status", folder, "Processing"))
base_name = os.path.basename(os.path.normpath(folder))
sanitized_base = sanitize_folder_name(base_name)
unique_output_dir = os.path.join(output_folder, f"{sanitized_base}_{base_name}")
os.makedirs(unique_output_dir, exist_ok=True)
try:
# Retrieve the selected output format
selected_format = output_format_var.get()
# Run the video2image command with the unique output directory and selected format
subprocess.run(
['video2image', '-i', folder, '-o', unique_output_dir, '-f', selected_format],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
progress_queue.put(("success", folder))
except subprocess.CalledProcessError as e:
error_msg = f"Failed to process folder: {folder}\nError Code: {e.returncode}"
progress_queue.put(("error", folder, error_msg))
except Exception as ex:
error_msg = f"An unexpected error occurred while processing folder: {folder}\nError: {ex}"
progress_queue.put(("error", folder, error_msg))
# Initialize ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(worker, tasks_queue.get()): tasks_queue.get() for _ in range(min(max_workers, tasks_queue.qsize()))}
while futures:
done, _ = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
for future in done:
folder = futures.pop(future)
try:
future.result()
except Exception as e:
error_msg = f"Error processing folder: {folder}\nError: {e}"
progress_queue.put(("error", folder, error_msg))
if not tasks_queue.empty() and not is_paused:
next_folder = tasks_queue.get()
futures[executor.submit(worker, next_folder)] = next_folder
# Finalize processing
finalize_processing(errors)
# Start disk space monitoring
monitor_disk_space()
def set_buttons_state(state):
process_button.config(state=state)
select_input_button.config(state=state)
select_output_button.config(state=state)
remove_button.config(state=NORMAL if input_folders and state == NORMAL else DISABLED)
pause_button.config(state=state if state == NORMAL else DISABLED)
resume_button.config(state=DISABLED if state == NORMAL else NORMAL)
output_format_combobox.config(state=DISABLED if state == DISABLED else NORMAL)
def on_pause():
global is_paused
is_paused = True
status_label.config(text="Processing paused.")
pause_button.config(state=DISABLED)
resume_button.config(state=NORMAL)
def on_resume():
global is_paused
is_paused = False
status_label.config(text="Resuming processing...")
pause_button.config(state=NORMAL)
resume_button.config(state=DISABLED)
# Resume task submission by re-invoking process_videos_thread
process_videos_thread()
def monitor_disk_space():
output_path = output_folder_var.get() or '/'
try:
total, used, free = psutil.disk_usage(output_path)
if free < disk_space_threshold_mb * 1024 * 1024:
if not is_paused:
on_pause()
Messagebox.show_warning("Auto-Pause", "Disk space below threshold. Processing has been paused.")
except Exception as e:
Messagebox.show_error("Disk Space Error", f"Unable to determine disk space for '{output_path}'.\nError: {e}")
root.after(DISK_CHECK_INTERVAL, monitor_disk_space)
def set_disk_threshold():
try:
value = int(disk_threshold_entry.get())
if value <= 0:
raise ValueError
global disk_space_threshold_mb
disk_space_threshold_mb = value
Messagebox.show_info("Threshold Set", f"Auto-pause threshold set to {value} MB.")
except ValueError:
Messagebox.show_error("Invalid Input", "Please enter a valid positive integer for the disk space threshold.")
def update_remove_button():
"""Enable or disable the remove button based on folder selection."""
if input_folders:
remove_button.config(state=NORMAL)
else:
remove_button.config(state=DISABLED)
def finalize_processing(errors):
set_buttons_state(NORMAL)
if errors:
error_message = "\n\n".join(errors)
Messagebox.show_error("Processing Completed with Errors", error_message)
else:
Messagebox.show_success("Success", "All videos have been successfully processed.")
progress_bar['value'] = 0
status_label.config(text="Processing completed.")
def update_progress():
try:
while True:
msg = progress_queue.get_nowait()
if msg[0] == "status":
_, folder, status = msg
tree.set(folder, "Status", status)
elif msg[0] == "success":
_, folder = msg
tree.set(folder, "Status", "Completed")
progress_bar['value'] += 1
elif msg[0] == "error":
_, folder, error_msg = msg
tree.set(folder, "Status", "Error")
errors.append(error_msg)
progress_bar['value'] += 1
# Update overall progress
overall = progress_bar['value']
status_label.config(text=f"Processed {overall} of {len(input_folders)} folders.")
if overall >= len(input_folders):
finalize_processing(errors)
except queue.Empty:
pass
if progress_bar['value'] < len(input_folders):
root.after(100, update_progress)
# Initialize the main window with ttkbootstrap
root = tb.Window(themename="darkly")
root.title(APP_NAME)
root.geometry("1100x800")
root.resizable(True, True) # Allow resizing for better usability
# Initialize the list to store input folders
input_folders = []
# Variable to store output folder path
output_folder_var = tb.StringVar()
# Variable for disk space threshold
disk_space_threshold_mb = 500 # Default to 500 MB
# Variable for output format
output_format_var = tb.StringVar(value="jpg") # Default to 'jpg'
# Pause state
is_paused = False
# Event to signal stopping of all tasks
stop_all_event = threading.Event()
# Configure styles using ttkbootstrap
style = tb.Style()
# Frame for Input Folders
input_frame = tb.LabelFrame(root, text="Input Folders")
input_frame.pack(fill="both", expand=True, padx=20, pady=10)
# Treeview to display selected input folders and their statuses
columns = ("Folder", "Status")
tree = tb.Treeview(input_frame, columns=columns, show='headings', selectmode='extended', height=15)
tree.heading("Folder", text="Folder")
tree.heading("Status", text="Status")
tree.column("Folder", anchor='w', width=800)
tree.column("Status", anchor='center', width=200)
tree.pack(side=tb.LEFT, fill=tb.BOTH, expand=True, padx=(0,5), pady=5)
# Scrollbar for the Treeview
tree_scroll = tb.Scrollbar(input_frame, orient=tb.VERTICAL, command=tree.yview)
tree_scroll.pack(side=tb.RIGHT, fill=tb.Y)
tree.config(yscrollcommand=tree_scroll.set)
# Frame for Input Buttons
input_button_frame = tb.Frame(root)
input_button_frame.pack(fill='x', padx=20, pady=(0,10))
# Button to select input folders
select_input_button = tb.Button(input_button_frame, text="Select Input Folders", bootstyle=PRIMARY, command=select_input_folders)
select_input_button.pack(side=tb.LEFT, padx=5)
# Button to remove selected input folders
remove_button = tb.Button(input_button_frame, text="Remove Selected", bootstyle=DANGER, command=remove_selected_folders, state=DISABLED)
remove_button.pack(side=tb.LEFT, padx=5)
# Frame for Output Folder
output_frame = tb.LabelFrame(root, text="Output Folder")
output_frame.pack(fill="both", expand=True, padx=20, pady=10)
# Entry widget to display selected output folder
output_entry = tb.Entry(output_frame, textvariable=output_folder_var, width=80, state='readonly')
output_entry.pack(side=tb.LEFT, padx=(0, 5), expand=True, fill=tb.X)
# Button to select output folder
select_output_button = tb.Button(output_frame, text="Select Output Folder", bootstyle=PRIMARY, command=select_output_folder)
select_output_button.pack(side=tb.LEFT, padx=5)
# Frame for Output Format Selection
format_frame = tb.LabelFrame(root, text="Output Format")
format_frame.pack(fill="x", expand=True, padx=20, pady=10)
format_label = tb.Label(format_frame, text="Select Output Format:")
format_label.pack(side=tb.LEFT, padx=(0,5))
output_format_combobox = tb.Combobox(format_frame, textvariable=output_format_var, values=["jpg", "png"], state="readonly")
output_format_combobox.pack(side=tb.LEFT, padx=5)
output_format_combobox.current(0) # Set default to 'jpg'
# Frame for Disk Space Threshold
disk_frame = tb.LabelFrame(root, text="Auto-Pause Disk Space Threshold (MB)")
disk_frame.pack(fill="x", expand=True, padx=20, pady=10)
disk_threshold_entry = tb.Entry(disk_frame, width=10, justify='center')
disk_threshold_entry.insert(0, str(disk_space_threshold_mb))
disk_threshold_entry.pack(side=tb.LEFT, padx=(0,5))
set_threshold_button = tb.Button(disk_frame, text="Set Threshold", bootstyle=SUCCESS, command=set_disk_threshold)
set_threshold_button.pack(side=tb.LEFT, padx=5)
# Progress Frame with Overall Progress Bar
progress_frame = tb.Frame(root)
progress_frame.pack(fill='x', padx=20, pady=10)
progress_label = tb.Label(progress_frame, text="Overall Progress:")
progress_label.pack(anchor='w')
progress_bar = tb.Progressbar(progress_frame, orient='horizontal', length=1000, mode='determinate', bootstyle="info")
progress_bar.pack(pady=5, fill=tb.X)
# Status Label
status_label = tb.Label(root, text="Ready.", relief=RAISED, anchor='w', bootstyle="secondary")
status_label.pack(fill='x', padx=20, pady=(0,10))
# Frame for Control Buttons
control_button_frame = tb.Frame(root)
control_button_frame.pack(fill='x', padx=20, pady=10)
# Process Button
process_button = tb.Button(control_button_frame, text="Process Videos", bootstyle=SUCCESS, command=process_videos_thread)
process_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)
# Pause Button
pause_button = tb.Button(control_button_frame, text="Pause", bootstyle=WARNING, command=on_pause, state=DISABLED)
pause_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)
# Resume Button
resume_button = tb.Button(control_button_frame, text="Resume", bootstyle=INFO, command=on_resume, state=DISABLED)
resume_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)
# Queue for inter-thread communication
progress_queue = queue.Queue()
# List to collect errors
errors = []
# Monitor changes in the input_folders list to update the remove button state
def monitor_input_folders():
"""Periodically check if input_folders is empty to enable/disable the remove button."""
update_remove_button()
root.after(500, monitor_input_folders)
monitor_input_folders()
# Periodically update the progress in the GUI
def periodic_update():
update_progress()
root.after(100, periodic_update)
periodic_update()
# Handle window closing to ensure all threads are properly terminated
def on_closing():
stop_all_event.set()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
# Run the GUI loop
root.mainloop()ASSISTANT
To add the ability to select individual video files alongside folders and process them similarly to how folders are handled, you will need to make several modifications to your existing Tkinter application. Below is the step-by-step guide along with the updated complete code incorporating the new functionality.
### **Key Enhancements:**
1. **Add "Select Videos" Button:**
- Introduce a new button that allows users to select individual video files.
2. **Update the TreeView:**
- Modify the existing TreeView to include a new "Type" column, distinguishing between folders and video files.
3. **Manage Separate Lists:**
- Maintain separate lists for input folders and input videos to manage them efficiently.
4. **Process Videos as Separate Tasks:**
- Update the processing logic to handle both folders and individual video files, treating each video as a separate task.
5. **Update UI Elements:**
- Ensure that UI elements such as progress bars, status labels, and buttons reflect the current state of processing, accommodating both folders and videos.
### **Updated Code:**
Below is the complete updated code incorporating the above enhancements:
```python
import ttkbootstrap as tb
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
from tkinter import filedialog
import os
import subprocess
import threading
import concurrent.futures
import queue
import psutil
import sys
# Constants for the application
APP_NAME = "DeepAscension Video2Image+"
DEFAULT_MAX_CONCURRENT_TASKS = 4
DISK_CHECK_INTERVAL = 5000 # in milliseconds
def select_input_folders():
while True:
folder = filedialog.askdirectory(title="Select Input Folder (Cancel to finish)")
if folder:
if folder not in input_folders and folder not in input_videos:
input_folders.append(folder)
tree.insert('', 'end', iid=folder, values=(folder, "Folder", "Pending"))
else:
Messagebox.show_warning("Duplicate Entry", f"The entry '{folder}' is already selected.")
else:
break
def select_input_videos():
files = filedialog.askopenfilenames(title="Select Video Files",
filetypes=[("Video Files", "*.mp4 *.avi *.mov *.mkv *.flv *.wmv")])
for file in files:
if file not in input_videos and file not in input_folders:
input_videos.append(file)
tree.insert('', 'end', iid=file, values=(file, "Video File", "Pending"))
else:
Messagebox.show_warning("Duplicate Entry", f"The file '{file}' is already selected.")
def remove_selected_items():
selected_items = tree.selection()
for item in selected_items:
item_type = tree.set(item, "Type")
if item_type == "Folder":
input_folders.remove(item)
elif item_type == "Video File":
input_videos.remove(item)
tree.delete(item)
update_remove_button()
def select_output_folder():
folder = filedialog.askdirectory(title="Select Output Folder")
if folder:
output_folder_var.set(folder)
output_entry.config(state='normal')
output_entry.delete(0, tb.END)
output_entry.insert(0, folder)
output_entry.config(state='readonly')
def sanitize_folder_name(name):
"""Sanitize the folder name to remove or replace problematic characters."""
return "".join(c if c.isalnum() or c in (' ', '_', '-') else "_" for c in name).replace(" ", "_")
def process_videos_thread():
"""Run the video processing in a separate thread."""
threading.Thread(target=process_videos, daemon=True).start()
def process_videos():
global is_paused
output_folder = output_folder_var.get()
if not input_folders and not input_videos:
Messagebox.show_error("Error", "Please select at least one input folder or video file.")
return
if not output_folder:
Messagebox.show_error("Error", "Please select an output folder.")
return
# Ensure the output directory exists
os.makedirs(output_folder, exist_ok=True)
# Disable the process button and folder selection buttons to prevent multiple clicks
set_buttons_state(DISABLED)
# Initialize overall progress bar
total_tasks = len(input_folders) + len(input_videos)
progress_bar['maximum'] = total_tasks
progress_bar['value'] = 0
status_label.config(text="Starting processing...")
# Queue to receive progress updates
progress_queue = queue.Queue()
# List to collect errors
errors = []
# Create a combined tasks queue
tasks_queue = queue.Queue()
for folder in input_folders:
tasks_queue.put(("Folder", folder))
for video in input_videos:
tasks_queue.put(("Video File", video))
# Determine optimal number of workers based on CPU cores
max_workers = min(os.cpu_count() * 2 or DEFAULT_MAX_CONCURRENT_TASKS, 32)
def worker(task_type, path):
if is_paused:
# Wait until resumed
while is_paused:
if stop_all_event.is_set():
return
threading.Event().wait(0.1)
# Update Treeview status to "Processing"
progress_queue.put(("status", path, "Processing"))
base_name = os.path.basename(os.path.normpath(path))
sanitized_base = sanitize_folder_name(base_name)
if task_type == "Folder":
unique_output_dir = os.path.join(output_folder, f"{sanitized_base}_{base_name}")
input_path = path
elif task_type == "Video File":
name, _ = os.path.splitext(base_name)
unique_output_dir = os.path.join(output_folder, f"{sanitize_folder_name(name)}_output")
input_path = path
os.makedirs(unique_output_dir, exist_ok=True)
try:
# Retrieve the selected output format
selected_format = output_format_var.get()
# Run the video2image command with the unique output directory and selected format
if task_type == "Folder":
subprocess.run(
['video2image', '-i', input_path, '-o', unique_output_dir, '-f', selected_format],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
elif task_type == "Video File":
subprocess.run(
['video2image', '-i', input_path, '-o', unique_output_dir, '-f', selected_format],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
progress_queue.put(("success", path))
except subprocess.CalledProcessError as e:
error_msg = f"Failed to process {task_type.lower()}: {path}\nError Code: {e.returncode}"
progress_queue.put(("error", path, error_msg))
except Exception as ex:
error_msg = f"An unexpected error occurred while processing {task_type.lower()}: {path}\nError: {ex}"
progress_queue.put(("error", path, error_msg))
# Initialize ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {}
for _ in range(min(max_workers, tasks_queue.qsize())):
if not tasks_queue.empty():
task_type, path = tasks_queue.get()
futures[executor.submit(worker, task_type, path)] = path
while futures:
done, _ = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
for future in done:
folder = futures.pop(future)
try:
future.result()
except Exception as e:
error_msg = f"Error processing {folder}\nError: {e}"
progress_queue.put(("error", folder, error_msg))
if not tasks_queue.empty() and not is_paused:
task_type, next_path = tasks_queue.get()
futures[executor.submit(worker, task_type, next_path)] = next_path
# Finalize processing
finalize_processing(errors)
# Start disk space monitoring
monitor_disk_space()
def set_buttons_state(state):
process_button.config(state=state)
select_input_button.config(state=state)
select_output_button.config(state=state)
select_videos_button.config(state=state)
remove_button.config(state=NORMAL if (input_folders or input_videos) and state == NORMAL else DISABLED)
pause_button.config(state=state if state == NORMAL else DISABLED)
resume_button.config(state=DISABLED if state == NORMAL else NORMAL)
output_format_combobox.config(state=DISABLED if state == DISABLED else NORMAL)
def on_pause():
global is_paused
is_paused = True
status_label.config(text="Processing paused.")
pause_button.config(state=DISABLED)
resume_button.config(state=NORMAL)
def on_resume():
global is_paused
is_paused = False
status_label.config(text="Resuming processing...")
pause_button.config(state=NORMAL)
resume_button.config(state=DISABLED)
# Resume task submission by re-invoking process_videos_thread
process_videos_thread()
def monitor_disk_space():
output_path = output_folder_var.get() or '/'
try:
total, used, free = psutil.disk_usage(output_path)
if free < disk_space_threshold_mb * 1024 * 1024:
if not is_paused:
on_pause()
Messagebox.show_warning("Auto-Pause", "Disk space below threshold. Processing has been paused.")
except Exception as e:
Messagebox.show_error("Disk Space Error", f"Unable to determine disk space for '{output_path}'.\nError: {e}")
root.after(DISK_CHECK_INTERVAL, monitor_disk_space)
def set_disk_threshold():
try:
value = int(disk_threshold_entry.get())
if value <= 0:
raise ValueError
global disk_space_threshold_mb
disk_space_threshold_mb = value
Messagebox.show_info("Threshold Set", f"Auto-pause threshold set to {value} MB.")
except ValueError:
Messagebox.show_error("Invalid Input", "Please enter a valid positive integer for the disk space threshold.")
def update_remove_button():
"""Enable or disable the remove button based on folder selection."""
if input_folders or input_videos:
remove_button.config(state=NORMAL)
else:
remove_button.config(state=DISABLED)
def finalize_processing(errors):
set_buttons_state(NORMAL)
if errors:
error_message = "\n\n".join(errors)
Messagebox.show_error("Processing Completed with Errors", error_message)
else:
Messagebox.show_success("Success", "All videos have been successfully processed.")
progress_bar['value'] = 0
status_label.config(text="Processing completed.")
def update_progress():
try:
while True:
msg = progress_queue.get_nowait()
if msg[0] == "status":
_, path, status = msg
tree.set(path, "Status", status)
elif msg[0] == "success":
_, path = msg
tree.set(path, "Status", "Completed")
progress_bar['value'] += 1
elif msg[0] == "error":
_, path, error_msg = msg
tree.set(path, "Status", "Error")
errors.append(error_msg)
progress_bar['value'] += 1
# Update overall progress
overall = progress_bar['value']
total = progress_bar['maximum']
status_label.config(text=f"Processed {overall} of {total} tasks.")
if overall >= total:
finalize_processing(errors)
except queue.Empty:
pass
if progress_bar['value'] < progress_bar['maximum']:
root.after(100, update_progress)
# Initialize the main window with ttkbootstrap
root = tb.Window(themename="darkly")
root.title(APP_NAME)
root.geometry("1200x800")
root.resizable(True, True) # Allow resizing for better usability
# Initialize the lists to store input folders and videos
input_folders = []
input_videos = []
# Variable to store output folder path
output_folder_var = tb.StringVar()
# Variable for disk space threshold
disk_space_threshold_mb = 500 # Default to 500 MB
# Variable for output format
output_format_var = tb.StringVar(value="jpg") # Default to 'jpg'
# Pause state
is_paused = False
# Event to signal stopping of all tasks
stop_all_event = threading.Event()
# Configure styles using ttkbootstrap
style = tb.Style()
# Frame for Input Folders and Videos
input_frame = tb.LabelFrame(root, text="Input Folders and Videos")
input_frame.pack(fill="both", expand=True, padx=20, pady=10)
# Treeview to display selected input folders and videos with their statuses
columns = ("Path", "Type", "Status")
tree = tb.Treeview(input_frame, columns=columns, show='headings', selectmode='extended', height=15)
tree.heading("Path", text="Path")
tree.heading("Type", text="Type")
tree.heading("Status", text="Status")
tree.column("Path", anchor='w', width=800)
tree.column("Type", anchor='center', width=100)
tree.column("Status", anchor='center', width=200)
tree.pack(side=tb.LEFT, fill=tb.BOTH, expand=True, padx=(0,5), pady=5)
# Scrollbar for the Treeview
tree_scroll = tb.Scrollbar(input_frame, orient=tb.VERTICAL, command=tree.yview)
tree_scroll.pack(side=tb.RIGHT, fill=tb.Y)
tree.config(yscrollcommand=tree_scroll.set)
# Frame for Input Buttons
input_button_frame = tb.Frame(root)
input_button_frame.pack(fill='x', padx=20, pady=(0,10))
# Button to select input folders
select_input_button = tb.Button(input_button_frame, text="Select Input Folders", bootstyle=PRIMARY, command=select_input_folders)
select_input_button.pack(side=tb.LEFT, padx=5)
# Button to select video files
select_videos_button = tb.Button(input_button_frame, text="Select Videos", bootstyle=PRIMARY, command=select_input_videos)
select_videos_button.pack(side=tb.LEFT, padx=5)
# Button to remove selected items
remove_button = tb.Button(input_button_frame, text="Remove Selected", bootstyle=DANGER, command=remove_selected_items, state=DISABLED)
remove_button.pack(side=tb.LEFT, padx=5)
# Frame for Output Folder
output_frame = tb.LabelFrame(root, text="Output Folder")
output_frame.pack(fill="both", expand=True, padx=20, pady=10)
# Entry widget to display selected output folder
output_entry = tb.Entry(output_frame, textvariable=output_folder_var, width=80, state='readonly')
output_entry.pack(side=tb.LEFT, padx=(0, 5), expand=True, fill=tb.X)
# Button to select output folder
select_output_button = tb.Button(output_frame, text="Select Output Folder", bootstyle=PRIMARY, command=select_output_folder)
select_output_button.pack(side=tb.LEFT, padx=5)
# Frame for Output Format Selection
format_frame = tb.LabelFrame(root, text="Output Format")
format_frame.pack(fill="x", expand=True, padx=20, pady=10)
format_label = tb.Label(format_frame, text="Select Output Format:")
format_label.pack(side=tb.LEFT, padx=(0,5))
output_format_combobox = tb.Combobox(format_frame, textvariable=output_format_var, values=["jpg", "png"], state="readonly")
output_format_combobox.pack(side=tb.LEFT, padx=5)
output_format_combobox.current(0) # Set default to 'jpg'
# Frame for Disk Space Threshold
disk_frame = tb.LabelFrame(root, text="Auto-Pause Disk Space Threshold (MB)")
disk_frame.pack(fill="x", expand=True, padx=20, pady=10)
disk_threshold_entry = tb.Entry(disk_frame, width=10, justify='center')
disk_threshold_entry.insert(0, str(disk_space_threshold_mb))
disk_threshold_entry.pack(side=tb.LEFT, padx=(0,5))
set_threshold_button = tb.Button(disk_frame, text="Set Threshold", bootstyle=SUCCESS, command=set_disk_threshold)
set_threshold_button.pack(side=tb.LEFT, padx=5)
# Progress Frame with Overall Progress Bar
progress_frame = tb.Frame(root)
progress_frame.pack(fill='x', padx=20, pady=10)
progress_label = tb.Label(progress_frame, text="Overall Progress:")
progress_label.pack(anchor='w')
progress_bar = tb.Progressbar(progress_frame, orient='horizontal', length=1000, mode='determinate', bootstyle="info")
progress_bar.pack(pady=5, fill=tb.X)
# Status Label
status_label = tb.Label(root, text="Ready.", relief=RAISED, anchor='w', bootstyle="secondary")
status_label.pack(fill='x', padx=20, pady=(0,10))
# Frame for Control Buttons
control_button_frame = tb.Frame(root)
control_button_frame.pack(fill='x', padx=20, pady=10)
# Process Button
process_button = tb.Button(control_button_frame, text="Process Videos", bootstyle=SUCCESS, command=process_videos_thread)
process_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)
# Pause Button
pause_button = tb.Button(control_button_frame, text="Pause", bootstyle=WARNING, command=on_pause, state=DISABLED)
pause_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)
# Resume Button
resume_button = tb.Button(control_button_frame, text="Resume", bootstyle=INFO, command=on_resume, state=DISABLED)
resume_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)
# Queue for inter-thread communication
progress_queue = queue.Queue()
# List to collect errors
errors = []
# Monitor changes in the input_folders and input_videos lists to update the remove button state
def monitor_input_folders_and_videos():
"""Periodically check if input_folders or input_videos is empty to enable/disable the remove button."""
update_remove_button()
root.after(500, monitor_input_folders_and_videos)
monitor_input_folders_and_videos()
# Periodically update the progress in the GUI
def periodic_update():
update_progress()
root.after(100, periodic_update)
periodic_update()
# Handle window closing to ensure all threads are properly terminated
def on_closing():
stop_all_event.set()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
# Run the GUI loop
root.mainloop()
```
### **Detailed Explanation of the Changes:**
1. **TreeView Update:**
- **Columns Added:** Changed the `columns` from `("Folder", "Status")` to `("Path", "Type", "Status")`.
- **Insertion of Items:** When inserting items into the TreeView, each entry now includes the path, type (`"Folder"` or `"Video File"`), and status (`"Pending"`).
2. **Selection of Video Files:**
- **New Button:** Added `Select Videos` button which calls the `select_input_videos` function.
- **Functionality:** This function allows users to select multiple video files. It checks for duplicates and inserts each selected video into the TreeView with type `"Video File"`.
3. **Removal of Items:**
- **Unified Removal:** The `remove_selected_items` function now handles removal of both folders and video files by checking the `Type` of each selected item.
4. **Processing Logic:**
- **Combined Task Queue:** Both folders and video files are added to a single `tasks_queue` with their respective types.
- **Worker Function:** The `worker` function now takes `task_type` (`"Folder"` or `"Video File"`) and processes them accordingly.
- **Folders:** Processed using the existing logic.
- **Video Files:** Treated as individual tasks, each video file is processed and its output is saved in a unique directory derived from the video's name.
- **Progress Tracking:** The progress bar and status labels now account for both folders and video files, providing a comprehensive overview of all tasks.
5. **UI Enhancements:**
- **Expanded Window Size:** Increased the window width to accommodate the additional column in the TreeView.
- **Status Updates:** Enhanced status messages to reflect whether a folder or video file is being processed, completed, or encountered an error.
- **Error Handling:** Improved error messages to distinguish between failures in processing folders and individual video files.
6. **Disk Space Monitoring:**
- **Functionality Remains:** The disk space monitoring feature continues to function as before, pausing processing if disk space falls below the specified threshold.
7. **Code Maintenance:**
- **Function Renaming:** Changed `remove_selected_folders` to `remove_selected_items` to reflect its expanded functionality.
- **Comments and Documentation:** Added and updated comments to make the code more understandable and maintainable.
### **Usage Instructions:**
1. **Selecting Inputs:**
- **Folders:** Click on the "Select Input Folders" button to choose one or more folders containing videos.
- **Video Files:** Click on the "Select Videos" button to choose individual video files.
2. **Output Settings:**
- **Output Folder:** Click on "Select Output Folder" to choose where the processed images will be saved.
- **Output Format:** Choose between "jpg" and "png" formats from the dropdown.
3. **Disk Space Threshold:**
- **Set Threshold:** Enter the minimum disk space (in MB) required for processing to continue. Processing will pause if disk space drops below this value.
4. **Processing:**
- **Start Processing:** Click on "Process Videos" to begin processing the selected folders and video files.
- **Pause/Resume:** Use the "Pause" and "Resume" buttons to control the processing flow as needed.
5. **Monitoring Progress:**
- **Progress Bar:** Shows the overall progress of tasks.
- **Status Labels:** Display the current status of individual tasks.
6. **Removing Inputs:**
- **Remove Selected:** Select one or more items in the TreeView and click on "Remove Selected" to delete them from the processing queue.
### **Conclusion:**
With these enhancements, your application now supports selecting and processing individual video files alongside folders. The processing logic treats each video file as a separate task, similar to processing separate folders, ensuring flexibility and efficiency in handling different types of inputs.
Feel free to further customize the UI and processing logic to better fit your specific requirements.