import sys import os import glob import re import subprocess import tkinter as tk from tkinter import ttk from tkinter import filedialog # Set path of par2j client_path = "../par2j64.exe" gui_path = "../MultiPar.exe" # Initialize global variables list_dir = [] dir_index = 0; current_dir = "./" list_par = [] list_src = [] list_size = [] list_now = [] # Check a folder exists in list already def check_dir_exist(one_path): global list_dir for list_item in list_dir: if os.path.samefile(one_path, list_item): return True return False # Search PAR2 sets in a folder def search_par_set(one_path): global list_par, list_src, list_size, list_now if one_path == "": return # Clear lists at first list_par.clear() list_src.clear() list_size.clear() list_now.clear() for item in treeview_list.get_children(): treeview_list.delete(item) found_base = "" button_check.config(state=tk.DISABLED) button_open.config(state=tk.DISABLED) treeview_list.heading('PAR', text="Items in PAR", anchor='center') treeview_list.heading('Now', text="Directory content", anchor='center') # Add found PAR sets for par_path in glob.glob(glob.escape(one_path) + "/*.par2"): file_name = os.path.basename(par_path) # Remove extension ".par2" one_name = os.path.splitext(file_name)[0] # Compare filename in case insensitive base_name = one_name.lower() # Remove ".vol#-#", ".vol#+#", or ".vol_#" at the last base_name = re.sub(r'[.]vol\d*[-+_]\d+$', "", base_name) # Confirm only one PAR2 set in the directory. if found_base != "": if base_name != found_base: list_par.clear() label_status.config(text= "There are multiple PAR sets in \"" + one_path + "\".") return else: found_base = base_name #print(file_name) list_par.append(file_name) if len(list_par) == 0: label_status.config(text= "PAR set isn't found in \"" + one_path + "\".") return label_status.config(text= "Directory = \"" + one_path + "\", PAR file = \"" + list_par[0] + "\"") button_check.config(state=tk.NORMAL) button_open.config(state=tk.NORMAL) # Check automatically root.after(100, button_check_clicked) # Select a folder to search PAR files def button_folder_clicked(): global current_dir, list_dir, dir_index if os.path.exists(current_dir) == False: current_dir = "./" search_dir = filedialog.askdirectory(initialdir=current_dir) if search_dir == "": return # This replacing seems to be worthless. #search_dir = search_dir.replace('\\', '/') # Insert the folder to list dir_count = len(list_dir) if dir_count == 0: dir_index = 0 list_dir.append(search_dir) dir_count += 1 else: # Check the folder exists already if check_dir_exist(search_dir): dir_index = list_dir.index(search_dir) else: dir_index += 1 list_dir.insert(dir_index, search_dir) dir_count += 1 if dir_count > 1: root.title('PAR Diff ' + str(dir_index + 1) + '/' + str(dir_count)) current_dir = os.path.dirname(search_dir) search_par_set(search_dir) # Read text and get source file def parse_text(output_text): global list_src, list_size multi_lines = output_text.split('\n') # Search starting point of list offset = multi_lines.index("Input File list\t:") offset += 2 # Get file size and name while offset < len(multi_lines): single_line = multi_lines[offset] if single_line == "": break # File size single_line = single_line.lstrip() fisrt_item = single_line.split()[0] file_size = int(fisrt_item) list_size.append(file_size) #print(file_size) # Compare filename in case insensitive file_name = single_line.split("\"")[1] file_name = file_name.lower() list_src.append(file_name) #print(file_name) offset += 1 # Check PAR files and list source files def button_check_clicked(): global list_dir, dir_index, list_par, list_src, list_size, list_now if len(list_dir) == 0: return treeview_list.heading('PAR', text="? items in PAR", anchor='center') treeview_list.heading('Now', text="Directory content", anchor='center') search_dir = list_dir[dir_index] if os.path.exists(search_dir) == False: label_status.config(text= "\"" + search_dir + "\" doesn't exist.") return if len(list_par) == 0: return if os.path.exists(client_path) == False: label_status.config(text= "Cannot call \"" + client_path + "\". Set path correctly.") return # Clear lists at first list_src.clear() list_size.clear() list_now.clear() for item in treeview_list.get_children(): treeview_list.delete(item) list_lost = [] add_count = 0 diff_count = 0 # Read source files in PAR set for par_path in list_par: # Call par2j's list command cmd = "\"" + client_path + "\" l /uo \"" + search_dir + "/" + par_path + "\"" res = subprocess.run(cmd, shell=True, capture_output=True, encoding='utf8') #print("return code: {}".format(res.returncode)) #print("captured stdout: {}".format(res.stdout)) if res.returncode == 0: #label_status.config(text= "Read \"" + par_path + "\" ok.") parse_text(res.stdout) break if (len(list_src) == 0) or (len(list_src) != len(list_size)): label_status.config(text= "Failed to read source files in the PAR set.") return # Get current directory-tree for dirs, subdirs, files in os.walk(search_dir): # Get sub-directory from base directory sub_dir = dirs.lstrip(search_dir) sub_dir = sub_dir.replace('\\', '/') sub_dir = sub_dir.lstrip('/') sub_dir = sub_dir.lower() if sub_dir != "": sub_dir += "/" # Add folders for dir_name in subdirs: item_name = sub_dir + dir_name.lower() + "/" list_now.append(item_name) #print("folder:", item_name) # Add files for file_name in files: item_name = sub_dir + file_name if (sub_dir == "") and (item_name in list_par): continue item_name = item_name.lower() list_now.append(item_name) #print("file:", item_name) # Make list of missing items for item_name in list_src: #print(item_name) if not item_name in list_now: # The item doesn't exit now. list_lost.append(item_name) list_now.append(item_name) # Compare lists to find additional items list_now.sort() for item_name in list_now: #print(item_name) if item_name.endswith("/"): # This item is a folder. if item_name in list_lost: treeview_list.insert(parent='', index='end', values=(item_name, ""), tags='red') elif item_name in list_src: if not bool_diff.get(): treeview_list.insert(parent='', index='end', values=(item_name, item_name)) else: find_flag = 0 for src_name in list_src: if src_name.startswith(item_name): # The folder exists as sub-directory. find_flag = 1 break; if find_flag == 0: # The folder doesn't exit in PAR set. treeview_list.insert(parent='', index='end', values=("", item_name), tags='blue') add_count += 1; else: # This item is a file. if item_name in list_lost: treeview_list.insert(parent='', index='end', values=(item_name, ""), tags='red') elif item_name in list_src: file_path = search_dir + "/" + item_name item_index = list_src.index(item_name) file_size = os.path.getsize(file_path) #print(item_index, list_size[item_index], file_size) if file_size == list_size[item_index]: if not bool_diff.get(): treeview_list.insert(parent='', index='end', values=(item_name, item_name)) else: treeview_list.insert(parent='', index='end', values=(item_name, item_name), tags='yellow') diff_count += 1 else: # The file doesn't exit in PAR set. treeview_list.insert(parent='', index='end', values=("", item_name), tags='blue') add_count += 1; # Number of missing or additional items item_count = len(list_src) lost_count = len(list_lost) if lost_count == 0: treeview_list.heading('PAR', text= str(item_count) + " items in PAR", anchor='center') else: treeview_list.heading('PAR', text= str(item_count) + " items in PAR ( " + str(lost_count) + " miss )", anchor='center') if add_count + diff_count > 0: if add_count == 0: treeview_list.heading('Now', text="Directory content ( " + str(diff_count) + " diff )", anchor='center') elif diff_count == 0: treeview_list.heading('Now', text="Directory content ( " + str(add_count) + " add )", anchor='center') else: treeview_list.heading('Now', text="Directory content ( " + str(add_count) + " add, " + str(diff_count) + " diff )", anchor='center') # If you want to see some summary, uncomment below section. #status_text = "Directory = \"" + search_dir + "\", PAR file = \"" + par_path + "\"\nTotal items = " + str(item_count) + ". " #if lost_count + add_count + diff_count == 0: # status_text += "Directory and Par File structure match." #else: # if lost_count > 0: # status_text += str(lost_count) + " items are missing. " # if add_count > 0: # status_text += str(add_count) + " items are additional. " # if diff_count > 0: # status_text += str(diff_count) + " items are different size." #label_status.config(text=status_text) # Open PAR set by MultiPar def button_open_clicked(): global list_dir, dir_index, list_par if len(list_dir) == 0: return search_dir = list_dir[dir_index] if os.path.exists(search_dir) == False: return if len(list_par) == 0: return if os.path.exists(gui_path) == False: label_status.config(text= "Cannot call \"" + gui_path + "\". Set path correctly.") return # Set command-line # Cover path by " for possible space par_path = search_dir + "/" + list_par[0] 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) # Move to next folder def button_next_clicked(): global current_dir, list_dir, dir_index dir_count = len(list_dir) search_dir = "" # Goto next directory while dir_count > 1: dir_index += 1 if dir_index >= dir_count: dir_index = 0 search_dir = list_dir[dir_index] # Check the directory exists if os.path.exists(search_dir): break else: list_dir.pop(dir_index) dir_index -= 1 dir_count -= 1 search_dir = "" if search_dir == "": return root.title('PAR Diff ' + str(dir_index + 1) + '/' + str(dir_count)) current_dir = os.path.dirname(search_dir) search_par_set(search_dir) # Window size and title root = tk.Tk() root.title('PAR Diff') root.minsize(width=480, height=240) # 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_next = ttk.Button(frame_top, text="Next folder", width=12, command=button_next_clicked) button_next.pack(side=tk.LEFT, padx=2) button_folder = ttk.Button(frame_top, text="Select folder", width=12, command=button_folder_clicked) button_folder.pack(side=tk.LEFT, padx=2) button_check = ttk.Button(frame_top, text="Check again", width=12, command=button_check_clicked, state=tk.DISABLED) button_check.pack(side=tk.LEFT, padx=2) button_open = ttk.Button(frame_top, text="Open with MultiPar", width=19, command=button_open_clicked, state=tk.DISABLED) button_open.pack(side=tk.LEFT, padx=2) bool_diff = tk.BooleanVar() bool_diff.set(False) # You may change initial state here. check_diff = ttk.Checkbutton(frame_top, variable=bool_diff, text="Diff only") check_diff.pack(side=tk.LEFT, padx=2) # List frame_middle = ttk.Frame(root, padding=(4,2,4,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) column = ('PAR', 'Now') treeview_list = ttk.Treeview(frame_middle, columns=column, selectmode='none') treeview_list.grid(row=0, column=0, sticky=(tk.E,tk.W,tk.S,tk.N)) treeview_list.column('#0',width=0, stretch='no') treeview_list.heading('#0', text='') treeview_list.heading('PAR', text="Items in PAR", anchor='center') treeview_list.heading('Now', text="Directory content", anchor='center') treeview_list.tag_configure('red', background='#FFC0C0') treeview_list.tag_configure('blue', background='#80FFFF') treeview_list.tag_configure('yellow', background='#FFFF80') scrollbar_list = ttk.Scrollbar(frame_middle, orient=tk.VERTICAL, command=treeview_list.yview) scrollbar_list.grid(row=0, column=1, sticky=(tk.N, tk.S)) treeview_list["yscrollcommand"] = scrollbar_list.set xscrollbar_list = ttk.Scrollbar(frame_middle, orient=tk.HORIZONTAL, command=treeview_list.xview) xscrollbar_list.grid(row=1, column=0, sticky=(tk.E, tk.W)) treeview_list["xscrollcommand"] = xscrollbar_list.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 folder to check difference.') label_status.pack(side=tk.LEFT, padx=2) # When folders are specified in command-line if len(sys.argv) > 1: search_dir = "" for one_argv in sys.argv[1:]: if os.path.isdir(one_argv): one_path = os.path.abspath(one_argv) one_path = one_path.replace('\\', '/') if check_dir_exist(one_path) == False: #print("argv=", one_path) list_dir.append(one_path) if search_dir == "": search_dir = one_path dir_count = len(list_dir) if dir_count > 1: root.title('PAR Diff 1/' + str(dir_count)) if search_dir != "": current_dir = os.path.dirname(search_dir) search_par_set(search_dir) # Show window root.mainloop()