import sys import os import subprocess import stat import tkinter as tk from tkinter import ttk from tkinter import filedialog # Set path of MultiPar client_path = "../par2j64.exe" gui_path = "../MultiPar.exe" # Set options for par2j # Because /fe option is set to exclude .PAR2 files by default, no need to set here. cmd_option = "/rr10 /rd2" # Initialize global variables current_dir = "./" sub_proc = None # Return zero for empty folder def check_empty(path='.'): total = 0 with os.scandir(path) as it: for entry in it: if entry.is_file(): # Ignore hidden file if entry.stat().st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN: continue # Ignore PAR file entry_ext = os.path.splitext(entry.name)[1] if entry_ext.lower() == ".par2": continue total += entry.stat().st_size elif entry.is_dir(): total += check_empty(entry.path) if total > 0: break return total # Search children folders or files in a parent folder def search_child_item(parent_path): parent_name = os.path.basename(parent_path) # Check the folder exists already item_count = listbox_list1.size() item_path = parent_path + "\\" item_index = 0 while item_index < item_count: index_path = listbox_list1.get(item_index) if os.path.samefile(item_path, index_path): label_status.config(text= "The folder \"" + parent_name + "\" is selected already.") return common_path = os.path.commonpath([item_path, index_path]) + "\\" if os.path.samefile(common_path, item_path): label_status.config(text= "The folder \"" + parent_name + "\" is parent of another selected item.") return if os.path.samefile(common_path, index_path): label_status.config(text= "The folder \"" + parent_name + "\" is child of another selected item.") return item_index += 1 # Add found items error_text = "" add_count = 0 for item_name in os.listdir(parent_path): # Ignore PAR files (extension ".par2") item_ext = os.path.splitext(item_name)[1] if item_ext.lower() == ".par2": error_text += " PAR file \"" + item_name + "\" is ignored." continue # Ignore hidden item item_path = os.path.join(parent_path, item_name) if os.stat(item_path).st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN: error_text += " Hidden \"" + item_name + "\" is ignored." continue # Distinguish folder or file if os.path.isdir(item_path): # Ignore empty folder if check_empty(item_path) == 0: continue item_path += "\\" listbox_list1.insert(tk.END, item_path) add_count += 1 item_count = listbox_list1.size() label_head1.config(text= str(item_count) + " items") if item_count == 0: label_status.config(text= "There are no items." + error_text) button_start.config(state=tk.DISABLED) elif add_count == 0: label_status.config(text= "No items were found in folder \"" + parent_name + "\"." + error_text) else: label_status.config(text= str(add_count) + " items were found in folder \"" + parent_name + "\"." + error_text) button_reset.config(state=tk.NORMAL) button_start.config(state=tk.NORMAL) button_remove.config(state=tk.NORMAL) # Select a folder to search children folders or files def button_parent_clicked(): global current_dir if os.path.exists(current_dir) == False: current_dir = "./" search_dir = filedialog.askdirectory(initialdir=current_dir) if search_dir == "": return current_dir = search_dir search_child_item(search_dir) # Reset lists and display status def button_reset_clicked(): global current_dir current_dir = "./" # Clear list-box at first listbox_list1.delete(0, tk.END) listbox_list2.delete(0, tk.END) # Reset statues text label_head1.config(text= '0 items') label_head2.config(text= '0 finished items') label_status.config(text= 'Select folders and/or files to create PAR files.') # Reset button state button_parent.config(state=tk.NORMAL) button_child.config(state=tk.NORMAL) button_file.config(state=tk.NORMAL) button_reset.config(state=tk.DISABLED) button_start.config(state=tk.DISABLED) button_stop.config(state=tk.DISABLED) button_remove.config(state=tk.DISABLED) button_open2.config(state=tk.DISABLED) # Check and add items def add_argv_item(): # Add specified items error_text = "" for one_path in sys.argv[1:]: # Make sure to be absolute path item_path = os.path.abspath(one_path) if os.path.exists(item_path) == False: error_text += " \"" + item_path + "\" doesn't exist." continue #print(item_path) # Ignore PAR files (extension ".par2") item_name = os.path.basename(item_path) item_ext = os.path.splitext(item_name)[1] if item_ext.lower() == ".par2": error_text += " PAR file \"" + item_name + "\" is ignored." continue # Ignore hidden item if os.stat(item_path).st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN: error_text += " Hidden \"" + item_name + "\" is ignored." continue # Distinguish folder or file if os.path.isdir(item_path): # Ignore empty folder if check_empty(item_path) == 0: continue item_path += "\\" # Check the item exists already or duplicates item_count = listbox_list1.size() item_index = 0 while item_index < item_count: index_path = listbox_list1.get(item_index) if os.path.samefile(item_path, index_path): error_text += " \"" + item_name + "\" is selected already." item_count = -1 break common_path = os.path.commonpath([item_path, index_path]) + "\\" if os.path.samefile(common_path, item_path): error_text += " \"" + item_name + "\" is parent of another selected item." item_count = -1 break if os.path.samefile(common_path, index_path): error_text += " \"" + item_name + "\" is child of another selected item." item_count = -1 break item_index += 1 if item_count < 0: continue listbox_list1.insert(tk.END, item_path) item_count = listbox_list1.size() label_head1.config(text= str(item_count) + " items") if item_count == 0: label_status.config(text= "There are no items." + error_text) else: label_status.config(text= str(item_count) + " items were selected at first." + error_text) button_reset.config(state=tk.NORMAL) button_start.config(state=tk.NORMAL) button_remove.config(state=tk.NORMAL) # Verify the first PAR set def queue_run(): global sub_proc if sub_proc != None: return if "disabled" in button_stop.state(): button_parent.config(state=tk.NORMAL) button_child.config(state=tk.NORMAL) button_file.config(state=tk.NORMAL) button_start.config(state=tk.NORMAL) button_remove.config(state=tk.NORMAL) button_open2.config(state=tk.NORMAL) label_status.config(text= "Stopped queue") return item_path = listbox_list1.get(0) if item_path == "": label_status.config(text= "There are no items.") return # When it's a folder, create PAR2 files for inner files. if item_path[-1:] == "\\": base_name = os.path.basename(item_path[:-1]) + ".par2" source_path = item_path + "*" par_path = item_path + base_name # When it's a file, create PAR2 files for the file. else: base_name = os.path.basename(item_path) + ".par2" source_path = item_path par_path = item_path + ".par2" label_status.config(text= "Creating \"" + base_name + "\"") # Set command-line # Cover path by " for possible space cmd = "\"" + client_path + "\" c /fe\"**.par2\" " + cmd_option + " \"" + par_path + "\" \"" + source_path + "\"" # If you want to see creating result only, use "t" command instead of "c". #print(cmd) # Run PAR2 client sub_proc = subprocess.Popen(cmd, shell=True) # Wait finish of creation root.after(300, queue_result) # Wait and read created result def queue_result(): global sub_proc # When sub-process was not started yet if sub_proc == None: return # When sub-process is running still exit_code = sub_proc.poll() if exit_code == None: # Call self again root.after(300, queue_result) return sub_proc = None item_path = listbox_list1.get(0) # When fatal error happened in par2j if exit_code == 1: button_parent.config(state=tk.NORMAL) button_child.config(state=tk.NORMAL) button_file.config(state=tk.NORMAL) button_reset.config(state=tk.NORMAL) button_stop.config(state=tk.DISABLED) button_remove.config(state=tk.NORMAL) button_open2.config(state=tk.NORMAL) label_status.config(text= "Failed queue") return # When you cancel par2j on Command Prompt elif exit_code == 2: button_parent.config(state=tk.NORMAL) button_child.config(state=tk.NORMAL) button_file.config(state=tk.NORMAL) button_reset.config(state=tk.NORMAL) button_start.config(state=tk.NORMAL) button_stop.config(state=tk.DISABLED) button_remove.config(state=tk.NORMAL) button_open2.config(state=tk.NORMAL) label_status.config(text= "Canceled queue") return # When par files were created successfully else: #print("exit code =", exit_code) # Add to list of finished items listbox_list2.insert(tk.END, item_path) item_count = listbox_list2.size() label_head2.config(text= str(item_count) + " finished items") # Remove the first item from the list listbox_list1.delete(0) # Process next set item_count = listbox_list1.size() if item_count == 0: button_parent.config(state=tk.NORMAL) button_child.config(state=tk.NORMAL) button_file.config(state=tk.NORMAL) button_reset.config(state=tk.NORMAL) button_stop.config(state=tk.DISABLED) button_open2.config(state=tk.NORMAL) label_status.config(text= "Created all items") elif "disabled" in button_stop.state(): button_parent.config(state=tk.NORMAL) button_child.config(state=tk.NORMAL) button_file.config(state=tk.NORMAL) button_reset.config(state=tk.NORMAL) button_start.config(state=tk.NORMAL) button_remove.config(state=tk.NORMAL) button_open2.config(state=tk.NORMAL) label_status.config(text= "Interrupted queue") else: root.after(100, queue_run) # Select a child folder to add def button_child_clicked(): global current_dir if os.path.exists(current_dir) == False: current_dir = "./" one_path = filedialog.askdirectory(initialdir=current_dir) if one_path == "": return current_dir = os.path.dirname(one_path) # Check the folder has content one_name = os.path.basename(one_path) if check_empty(one_path) == 0: label_status.config(text= "Selected folder \"" + one_name + "\" is empty.") return # Check the folder is new one_path += "\\" item_count = listbox_list1.size() item_index = 0 while item_index < item_count: index_path = listbox_list1.get(item_index) if os.path.samefile(one_path, index_path): label_status.config(text= "Folder \"" + one_name + "\" is selected already.") return common_path = os.path.commonpath([one_path, index_path]) + "\\" if os.path.samefile(common_path, one_path): label_status.config(text= "Folder \"" + one_name + "\" is parent of another selected item.") return if os.path.samefile(common_path, index_path): label_status.config(text= "Folder \"" + one_name + "\" is child of another selected item.") return item_index += 1 listbox_list1.insert(tk.END, one_path) item_count += 1 label_head1.config(text= str(item_count) + " items") label_status.config(text= "Folder \"" + one_name + "\" was added.") button_reset.config(state=tk.NORMAL) button_start.config(state=tk.NORMAL) button_remove.config(state=tk.NORMAL) # Select multiple children files to add def button_file_clicked(): global current_dir if os.path.exists(current_dir) == False: current_dir = "./" multi_path = filedialog.askopenfilenames(initialdir=current_dir) if len(multi_path) == 0: return one_path = multi_path[0] current_dir = os.path.dirname(one_path) # Add selected items error_text = "" add_count = 0 for one_path in multi_path: # Ignore PAR file (extension ".par2") one_name = os.path.basename(one_path) item_ext = os.path.splitext(one_name)[1] if item_ext.lower() == ".par2": error_text += " PAR file \"" + one_name + "\" is ignored." continue # Ignore hidden file if os.stat(one_path).st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN: error_text += " Hidden \"" + one_name + "\" is ignored." continue # Check the file is new item_count = listbox_list1.size() item_index = 0 while item_index < item_count: index_path = listbox_list1.get(item_index) if os.path.samefile(one_path, index_path): error_text += " \"" + one_name + "\" is selected already." item_count = -1 break common_path = os.path.commonpath([one_path, index_path]) + "\\" if os.path.samefile(common_path, index_path): error_text += " \"" + one_name + "\" is child of another selected item." item_count = -1 break item_index += 1 if item_count < 0: continue add_name = one_name listbox_list1.insert(tk.END, one_path) add_count += 1 item_count = listbox_list1.size() label_head1.config(text= str(item_count) + " items") if item_count == 0: label_status.config(text= "There are no items." + error_text) button_start.config(state=tk.DISABLED) elif add_count == 0: label_status.config(text= "No files were added." + error_text) else: if add_count == 1: label_status.config(text= "File \"" + add_name + "\" was added." + error_text) else: label_status.config(text= str(add_count) + " files were added." + error_text) button_reset.config(state=tk.NORMAL) button_start.config(state=tk.NORMAL) button_remove.config(state=tk.NORMAL) # Select a child file to add def button_file1_clicked(): global current_dir if os.path.exists(current_dir) == False: current_dir = "./" one_path = filedialog.askopenfilename(initialdir=current_dir) if one_path == "": return current_dir = os.path.dirname(one_path) # Ignore PAR file (extension ".par2") one_name = os.path.basename(one_path) item_ext = os.path.splitext(one_name)[1] # Compare filename in case insensitive item_ext = item_ext.lower() if item_ext == ".par2": label_status.config(text= "PAR file \"" + one_name + "\" is ignored.") return # Ignore hidden file if os.stat(one_path).st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN: label_status.config(text= "Hidden \"" + one_name + "\" is ignored.") return # Check the file is new item_count = listbox_list1.size() item_index = 0 while item_index < item_count: index_path = listbox_list1.get(item_index) if os.path.samefile(one_path, index_path): label_status.config(text= "File \"" + one_name + "\" is selected already.") return common_path = os.path.commonpath([one_path, index_path]) + "\\" if os.path.samefile(common_path, index_path): label_status.config(text= "File \"" + one_name + "\" is child of another selected item.") return item_index += 1 listbox_list1.insert(tk.END, one_path) item_count += 1 label_head1.config(text= str(item_count) + " items") label_status.config(text= "File \"" + one_name + "\" was added.") button_reset.config(state=tk.NORMAL) button_start.config(state=tk.NORMAL) button_remove.config(state=tk.NORMAL) # Resume stopped queue def button_start_clicked(): global sub_proc item_count = listbox_list1.size() if item_count == 0: label_status.config(text= "There are no items.") return button_parent.config(state=tk.DISABLED) button_child.config(state=tk.DISABLED) button_file.config(state=tk.DISABLED) button_reset.config(state=tk.DISABLED) button_start.config(state=tk.DISABLED) button_stop.config(state=tk.NORMAL) button_remove.config(state=tk.DISABLED) button_open2.config(state=tk.DISABLED) if sub_proc == None: queue_run() else: queue_result() # Stop running queue def button_stop_clicked(): button_stop.config(state=tk.DISABLED) if sub_proc != None: label_status.config(text= "Waiting finish of current task") # Remove items from list def button_remove_clicked(): # It's possible to select multiple items. selected_indices = listbox_list1.curselection() selected_count = len(selected_indices) if selected_count == 0: label_status.config(text= "Select items to remove at first.") return label_status.config(text= "Removed " + str(selected_count) + " items.") while selected_count > 0: selected_count -= 1 selected_index = selected_indices[selected_count] # Remove selected items at once listbox_list1.delete(selected_index) item_count = listbox_list1.size() label_head1.config(text= str(item_count) + " items") if item_count == 0: button_start.config(state=tk.DISABLED) button_remove.config(state=tk.DISABLED) # Open a PAR set by MultiPar def button_open2_clicked(): if os.path.exists(gui_path) == False: label_status.config(text= "Cannot call \"" + gui_path + "\". Set path correctly.") return indices = listbox_list2.curselection() if len(indices) == 1: item_path = listbox_list2.get(indices[0]) if item_path[-1:] == "\\": base_name = os.path.basename(item_path[:-1]) + ".par2" par_path = item_path + base_name else: base_name = os.path.basename(item_path) + ".par2" par_path = item_path + ".par2" label_status.config(text= "Opening \"" + base_name + "\"") # Set command-line # Cover path by " for possible space cmd = "\"" + gui_path + "\" /verify \"" + par_path + "\"" # Open MultiPar GUI to see details # Because this doesn't wait finish of MultiPar, you may open some at once. subprocess.Popen(cmd) else: label_status.config(text= "Select one item to open at first.") # Window size and title root = tk.Tk() root.title('PAR Queue - Create') root.minsize(width=480, height=200) # Centering window init_width = 640 init_height = 480 init_left = (root.winfo_screenwidth() - init_width) // 2 init_top = (root.winfo_screenheight() - init_height) // 2 root.geometry('{}x{}+{}+{}'.format(init_width, init_height, init_left, init_top)) #root.geometry("640x480") root.columnconfigure(0, weight=1) root.rowconfigure(1, weight=1) # Control panel frame_top = ttk.Frame(root, padding=(3,4,3,2)) frame_top.grid(row=0, column=0, sticky=(tk.E,tk.W)) button_parent = ttk.Button(frame_top, text="Search inner folder", width=18, command=button_parent_clicked) button_parent.pack(side=tk.LEFT, padx=2) button_child = ttk.Button(frame_top, text="Add single folder", width=16, command=button_child_clicked) button_child.pack(side=tk.LEFT, padx=2) button_file = ttk.Button(frame_top, text="Add multi files", width=14, command=button_file_clicked) button_file.pack(side=tk.LEFT, padx=2) button_reset = ttk.Button(frame_top, text="Reset lists", width=11, command=button_reset_clicked, state=tk.DISABLED) button_reset.pack(side=tk.LEFT, padx=2) # List frame_middle = ttk.Frame(root, padding=(2,2,2,2)) frame_middle.grid(row=1, column=0, sticky=(tk.E,tk.W,tk.S,tk.N)) frame_middle.rowconfigure(0, weight=1) frame_middle.columnconfigure(0, weight=1) frame_middle.columnconfigure(1, weight=1) # List of children items (folders and files) frame_list1 = ttk.Frame(frame_middle, padding=(6,2,6,6), relief='groove') frame_list1.grid(row=0, column=0, padx=4, sticky=(tk.E,tk.W,tk.S,tk.N)) frame_list1.columnconfigure(0, weight=1) frame_list1.rowconfigure(2, weight=1) frame_top1 = ttk.Frame(frame_list1, padding=(0,4,0,3)) frame_top1.grid(row=0, column=0, columnspan=2, sticky=(tk.E,tk.W)) button_start = ttk.Button(frame_top1, text="Start", width=6, command=button_start_clicked, state=tk.DISABLED) button_start.pack(side=tk.LEFT, padx=2) button_stop = ttk.Button(frame_top1, text="Stop", width=6, command=button_stop_clicked, state=tk.DISABLED) button_stop.pack(side=tk.LEFT, padx=2) button_remove = ttk.Button(frame_top1, text="Remove", width=8, command=button_remove_clicked, state=tk.DISABLED) button_remove.pack(side=tk.LEFT, padx=2) label_head1 = ttk.Label(frame_list1, text='0 items') label_head1.grid(row=1, column=0, columnspan=2) s_list1 = tk.StringVar() listbox_list1 = tk.Listbox(frame_list1, listvariable=s_list1, activestyle='none', selectmode='extended') listbox_list1.grid(row=2, column=0, sticky=(tk.E,tk.W,tk.S,tk.N)) scrollbar_list1 = ttk.Scrollbar(frame_list1, orient=tk.VERTICAL, command=listbox_list1.yview) scrollbar_list1.grid(row=2, column=1, sticky=(tk.N, tk.S)) listbox_list1["yscrollcommand"] = scrollbar_list1.set xscrollbar_list1 = ttk.Scrollbar(frame_list1, orient=tk.HORIZONTAL, command=listbox_list1.xview) xscrollbar_list1.grid(row=3, column=0, sticky=(tk.E, tk.W)) listbox_list1["xscrollcommand"] = xscrollbar_list1.set # List of finished items frame_list2 = ttk.Frame(frame_middle, padding=(6,2,6,6), relief='groove') frame_list2.grid(row=0, column=1, padx=4, sticky=(tk.E,tk.W,tk.S,tk.N)) frame_list2.columnconfigure(0, weight=1) frame_list2.rowconfigure(2, weight=1) frame_top2 = ttk.Frame(frame_list2, padding=(0,4,0,3)) frame_top2.grid(row=0, column=0, columnspan=2, sticky=(tk.E,tk.W)) button_open2 = ttk.Button(frame_top2, text="Open with MultiPar", width=20, command=button_open2_clicked, state=tk.DISABLED) button_open2.pack(side=tk.LEFT, padx=2) label_head2 = ttk.Label(frame_list2, text='0 finished items') label_head2.grid(row=1, column=0, columnspan=2) s_list2 = tk.StringVar() listbox_list2 = tk.Listbox(frame_list2, listvariable=s_list2, activestyle='none') listbox_list2.grid(row=2, column=0, sticky=(tk.E,tk.W,tk.S,tk.N)) scrollbar_list2 = ttk.Scrollbar(frame_list2, orient=tk.VERTICAL, command=listbox_list2.yview) scrollbar_list2.grid(row=2, column=1, sticky=(tk.N, tk.S)) listbox_list2["yscrollcommand"] = scrollbar_list2.set xscrollbar_list2 = ttk.Scrollbar(frame_list2, orient=tk.HORIZONTAL, command=listbox_list2.xview) xscrollbar_list2.grid(row=3, column=0, sticky=(tk.E, tk.W)) listbox_list2["xscrollcommand"] = xscrollbar_list2.set # Status text frame_bottom = ttk.Frame(root) frame_bottom.grid(row=2, column=0, sticky=(tk.E,tk.W)) label_status = ttk.Label(frame_bottom, text='Select folders and/or files to create PAR files.') label_status.pack(side=tk.LEFT, padx=2) # When a folder is specified in command-line if len(sys.argv) == 2: one_path = os.path.abspath(sys.argv[1]) if os.path.isdir(one_path): search_child_item(one_path) else: add_argv_item() # When multiple items are specified elif len(sys.argv) > 2: add_argv_item() # If you want to start creation automatically, use below lines. #if listbox_list1.size() > 0: # button_parent.config(state=tk.DISABLED) # button_child.config(state=tk.DISABLED) # button_file.config(state=tk.DISABLED) # button_stop.config(state=tk.NORMAL) # root.after(100, queue_run) # Show window root.mainloop()