diff --git a/alpha/tool/diff_folder.py b/alpha/tool/diff_folder.py new file mode 100644 index 0000000..4d41e71 --- /dev/null +++ b/alpha/tool/diff_folder.py @@ -0,0 +1,428 @@ +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() diff --git a/alpha/tool/each_folder.py b/alpha/tool/each_folder.py new file mode 100644 index 0000000..1dee083 --- /dev/null +++ b/alpha/tool/each_folder.py @@ -0,0 +1,327 @@ +import sys +import os +import subprocess + +# This script is based on a script provided by Project Maintainer: Yukata-Sawada. I (markxu98) modified it for my own use. I tested it only on my machines +# with only my own files. Although I believe the structure of my files should reflex many different types of users/organizations, I cannot guarantee this +# script is suitable for ALL. So I suggest use it, understand it, and modify it by yourself to fit. + +# The purpose of this script is to generate PAR2 files with (somewhat) "best" storage efficiency with (somewhat) "good" reliability, while spending +# (somewhat) "less" time or using (somewhat) "less" processing power. + +# The following "const" values are optimized by me. They should be okay to most cases. Use caution when changing all of them. I added more comments to +# certain ones important to calculation. + +# Set path of par2j +client_path = "../par2j64.exe" + +# Set options for par2j +# Don't use /ss, /sn, /sr, or /sm here. +cmd_option = "/rr10 /rd1 /rf3" + +# How to set slices initially (either /ss, /sn, or /sr) +# The default setting is /sr10 in MultiPar. +init_slice_option = "/sr10" + +# Slice size multiplier (used in all cmd in this script) +# It would be good to set a cluster size of HDD/SSD. (4k == 4096 by default) +slice_size_multiplier = 4096 + +# Max number of slices at searching good efficiency (20000 by default). +# More number of slices is good for many files or varied size. +# This value can be ignored if initial slice count is even larger. +# Set this value to 32768 to ignore it completely. +max_slice_count = 20000 + +# Max percent of slices at searching good efficiency (170% by default). +# If initial_count is less than max_slice_count, the maximum slice count at +# searching is the smaller value of max_slice_count and +# (initial_count * max_slice_rate / 100). +# If initial_count is not less than max_slice_count, max_slice_count is +# ignored, and the maximum slice count at searching is the smaller value of +# 32768 and (initial_count * max_slice_rate / 100). +# This will also be used to calculate the min_slice_size. +max_slice_rate = 170 +#max_slice_rate = 0 +# If you have good processing power, you can set above rate to 0, then BOTH +# max_slice_count and max_slice_rate will be ignored. 32768 will be fixed as +# max_slice_count at searching. + +# Min number of slices at searching good efficiency (100 by default). +# Normally less number of slices tend to archive higher efficiency. +# But, too few slices is bad against random error. +# If by very small chance, the calculated maximum slice count is less than +# min_slice_count, no searching is needed and min_slice_count is used to +# generate PAR2 file. +min_slice_count = 100 + +# Min percent of slices at searching good efficiency (30% by default). +# Normally less number of slices tend to archive higher efficiency. +# But, too few slices is bad against random error. It may need at least 50%. +# If initial_count is more than min_slice_count, the minimum slice count at +# searching is the greater value of min_slice_count and +# (initial_count * min_slice_rate / 100). +# If initial_count is not more than min_slice_count, min_slice_rate is +# ignored, and the minimum slice count at searching is min_slice_count. +# This will also be used to calculate the max_slice_size. +min_slice_rate = 30 +#min_slice_rate = 0 +# If you have good processing power, you can set above rate to 0, then this +# rate will be ignored and min_slice_count is used at searching + +# Caution: You CAN set max_slice_rate = 0 and min_slice_rate = 0 at the same +# time to search (almost) "WHOLE" range, from min_slice_count to 32768. + +# Min efficiency improvment that will be regarded as "better" (0.3% by default). +# If the efficiency improvement is not so significant, it's unreasonable to +# use a larger slice count. This value controls how significant to update the +# best slice count at searching. +min_efficiency_improvement = 0.3 +#min_efficiency_improvement = 0 +# If you want to achieve "absolute" best efficiency, you can set above to 0. + +# Read "Efficiency rate" +def read_efficiency(output_text): + # Find from the last + line_start = output_text.rfind("Efficiency rate\t\t:") + if line_start != -1: + line_start += 19 + line_end = output_text.find("%\n", line_start) + #print("line_start=", line_start) + #print("line_end=", line_end) + return float(output_text[line_start:line_end]) + else: + return -1 + + +# Read "Input File Slice count" +def read_slice_count(output_text): + # Find from the top + line_start = output_text.find("Input File Slice count\t:") + if line_start != -1: + line_start += 25 + line_end = output_text.find("\n", line_start) + return int(output_text[line_start:line_end]) + else: + return -1 + + +# Read "Input File Slice size" +def read_slice_size(output_text): + # Find from the top + line_start = output_text.find("Input File Slice size\t:") + if line_start != -1: + line_start += 24 + line_end = output_text.find("\n", line_start) + return int(output_text[line_start:line_end]) + else: + return -1 + + +# Search setting of good efficiency +def test_efficiency(par_path): + min_size = 0 + max_size = 0 + best_count = 0 + best_size = 0 + best_efficiency = 0 + best_efficiency_at_initial_count = 0 + best_count_at_max_count = 0 + best_size_at_max_count = 0 + best_efficiency_at_max_count = 0 + + # First time to get initial value + cmd = "\"" + client_path + "\" t /uo " + init_slice_option + " /sm" + str(slice_size_multiplier) + " " + cmd_option + " \"" + 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: + return 0 + efficiency_rate = read_efficiency(res.stdout) + if efficiency_rate < 0: + return 0 + # DON'T change best_count, best_size and best_efficiency here. The following three values will be evaluated after the search is done. + # Using initial_count may not be best case. If the search can find a slice count less than initial_count, whose efficiency is the same as + # best_efficiency_at_initial_count, that slice count should be used instead of initial_count. + initial_count = read_slice_count(res.stdout) + if initial_count <= 0: + return 0 + initial_size = read_slice_size(res.stdout) + best_efficiency_at_initial_count = efficiency_rate + #print("initial_size =", initial_size, ", initial_count =", initial_count, ", efficiency =", efficiency_rate) + + # Get min and max of slice count and size to be used at searching + # maximum slice count is co-related to minimum slice size + if max_slice_rate != 0: + if initial_count > max_slice_count: + if (initial_count * max_slice_rate / 100) > 32768: + max_count = 32768 + else: + max_count = int(initial_count * max_slice_rate / 100) + else: + if (initial_count * max_slice_rate / 100) > max_slice_count: + max_count = max_slice_count + else: + max_count = int(initial_count * max_slice_rate / 100) + else: + max_count = 32768 + # Giving out the calculated maximum slice count, get "real" max_count and min_size from result of Par2j64.exe + # Here use option "/sn" to search around (from -12.5% to +6.25%) the calculated maximum slice count for best efficiency + cmd = "\"" + client_path + "\" t /uo /sn" + str(max_count) + " /sm" + str(slice_size_multiplier) + " " + cmd_option + " \"" + par_path + "\" *" + res = subprocess.run(cmd, shell=True, capture_output=True, encoding='utf8') + if res.returncode != 0: + return 0 + efficiency_rate = read_efficiency(res.stdout) + if efficiency_rate < 0: + return 0 + # DON'T change best_count, best_size and best_efficiency here. The following three values will be evaluated after the search is done. + # Using max_count is the worst case as it will require more processing power. If the search can find a slice count less than max_count, + # whose efficiency is the same as best_efficiency_at_max_count, that slice count should be used instead of best_count_at_max_count. + best_count_at_max_count = read_slice_count(res.stdout) + best_size_at_max_count = read_slice_size(res.stdout) + best_efficiency_at_max_count = efficiency_rate + max_count = read_slice_count(res.stdout) + min_size = read_slice_size(res.stdout) + #print("max_count =", max_count, ", min_size =", min_size, ", efficiency =", best_efficiency_at_max_count) + + # Minimum slice count is co-related to maximum slice size + if min_slice_rate > 0 and (initial_count * min_slice_rate / 100) > min_slice_count: + min_count = int(initial_count * min_slice_rate / 100) + else: + min_count = min_slice_count + # Giving out the calculated minimum slice count, get "real" min_count and max_size from result of Par2j64.exe + # Here use option "/sn" to search around (from -12.5% to +6.25%) the calculated minimum slice count for best efficiency + cmd = "\"" + client_path + "\" t /uo /sn" + str(min_count) + " /sm" + str(slice_size_multiplier) + " " + cmd_option + " \"" + par_path + "\" *" + res = subprocess.run(cmd, shell=True, capture_output=True, encoding='utf8') + if res.returncode != 0: + return 0 + efficiency_rate = read_efficiency(res.stdout) + if efficiency_rate < 0: + return 0 + min_count = read_slice_count(res.stdout) + max_size = read_slice_size(res.stdout) + best_count = read_slice_count(res.stdout) + best_size = read_slice_size(res.stdout) + best_efficiency = efficiency_rate + #print("min_count =", min_count, ", max_size =", max_size, ", efficiency =", best_efficiency) + + # If the calculated maximum slice count is too small, no need to search (QUITE UNLIKELY to happen) + if max_slice_rate > 0 and (initial_count * max_slice_rate / 100) <= min_slice_count: + # Giving out min_slice_count, get "real" best_size from result of Par2j64.exe + # Here use option "/sn" to search around (from -12.5% to +6.25%) the minimum slice count for best efficiency + cmd = "\"" + client_path + "\" t /uo /sn" + str(min_slice_count) + " /sm" + str(slice_size_multiplier) + " " + cmd_option + " \"" + par_path + "\" *" + res = subprocess.run(cmd, shell=True, capture_output=True, encoding='utf8') + if res.returncode != 0: + return 0 + efficiency_rate = read_efficiency(res.stdout) + if efficiency_rate < 0: + return 0 + best_count = read_slice_count(res.stdout) + best_size = read_slice_size(res.stdout) + best_efficiency = efficiency_rate + #print("initial_count too small, best_count =", best_count, ", best_size =", best_size, ", best_efficiency =", best_efficiency) + # Return slice size to archive the best efficiency + return best_size + else: + # Try every (step) slice count between min_count and max_count + step_slice_count_int = int((min_count + 1) * 8 / 7) + while step_slice_count_int < max_count: + #print(f"Testing slice count: (around) {step_slice_count_int}, from {(step_slice_count_int - int(step_slice_count_int / 8))} to {int(step_slice_count_int * 17 / 16)}") + # Giving out the calculated step slice count, get "real" slice count and size from result of Par2j64.exe + # Here use option "/sn" to search around (from -12.5% to +6.25%) the calculated step slice count for best efficiency + cmd = "\"" + client_path + "\" t /uo /sn" + str(step_slice_count_int) + " /sm" + str(slice_size_multiplier) + " " + cmd_option + " \"" + par_path + "\" *" + res = subprocess.run(cmd, shell=True, capture_output=True, encoding='utf8') + if res.returncode != 0: + break + efficiency_rate = read_efficiency(res.stdout) + if efficiency_rate < 0: + break + if efficiency_rate > best_efficiency + min_efficiency_improvement: + best_count = read_slice_count(res.stdout) + best_size = read_slice_size(res.stdout) + best_efficiency = efficiency_rate + # Next count should be more than 17/16 of the input count. (Range to +6.25% was checked already.) + step_slice_count_int = int((int(step_slice_count_int * 17 / 16) + 1) * 8 / 7) + # Evaluate slice count searched with initial_count + if initial_count < best_count and best_efficiency_at_initial_count > best_efficiency - min_efficiency_improvement: + best_count = initial_count + best_size = initial_size + best_efficiency = best_efficiency_at_initial_count + # Evaluate slice count searched with max_count. + if best_efficiency_at_max_count > best_efficiency + min_efficiency_improvement: + best_count = best_count_at_max_count + best_size = best_size_at_max_count + best_efficiency = best_efficiency_at_max_count + #print("best_count =", best_count, "best_size =", best_size, ", best_efficiency =", best_efficiency) + + return best_size + + +# Return sub-process's ExitCode +def command(cmd): + ret = subprocess.run(cmd, shell=True) + return ret.returncode + + +# 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(): + total += entry.stat().st_size + elif entry.is_dir(): + total += check_empty(entry.path) + if total > 0: + break + return total + + +# Read arguments of command-line +for idx, arg in enumerate(sys.argv[1:]): + one_path = arg + one_name = os.path.basename(one_path) + + # Check the folder exists + if os.path.isdir(one_path) == False: + print(one_name + " isn't folder.") + continue + + # Check empty folder + if check_empty(one_path) == 0: + print(one_name + " is empty folder.") + continue + + print(one_name + " is folder.") + + # Path of creating PAR file + par_path = one_path + "\\" + one_name + ".par2" + + # Check the PAR file exists already + if os.path.exists(par_path): + print(one_name + " includes PAR file already.") + continue + + # Test setting for good efficiency + slice_size = test_efficiency(par_path) + if slice_size == 0: + print("Failed to test options.") + continue + + # Set command-line + # Cover path by " for possible space + cmd = "\"" + client_path + "\" c /ss" + str(slice_size) + " " + cmd_option + " \"" + par_path + "\" *" + # If you want to see creating result only, use "t" command instead of "c". + + # Process the command + print("Creating PAR files.") + error_level = command(cmd) + + # Check error + # Exit loop, when error occur. + if error_level > 0: + print("Error=", error_level) + break + +# If you don't confirm result, comment out below line. +input('Press [Enter] key to continue . . .') diff --git a/alpha/tool/group_files.py b/alpha/tool/group_files.py new file mode 100644 index 0000000..e4b99f9 --- /dev/null +++ b/alpha/tool/group_files.py @@ -0,0 +1,141 @@ +import sys +import os +import subprocess + + +# Set path of par2j +client_path = "../par2j64.exe" + +# Set path of file-list +list_path = "../save/file-list.txt" + + +# Return sub-process's ExitCode +def command(cmd): + ret = subprocess.run(cmd, shell=True) + return ret.returncode + + +# 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(): + total += entry.stat().st_size + elif entry.is_dir(): + total += check_empty(entry.path) + if total > 0: + break + return total + + +# Read arguments of command-line +for idx, arg in enumerate(sys.argv[1:]): + one_path = arg + one_name = os.path.basename(one_path) + + # Check the folder exists + if os.path.isdir(one_path) == False: + print(one_name + " isn't folder.") + continue + + # Check empty folder + if check_empty(one_path) == 0: + print(one_name + " is empty folder.") + continue + + print(one_name + " is folder.") + + # Check the PAR file exists already + par_path = one_path + "\\#1.par2" + if os.path.exists(par_path): + print(one_name + " includes PAR files already.") + continue + + # Create PAR file for each 1000 source files. + group_index = 1 + file_count = 0 + error_level = 0 + + # Set options for par2j + par_option = "/rr10 /rd2" + + # Make file-list of source files in a folder + f = open(list_path, 'w', encoding='utf-8') + + # Search inner directories and files + for cur_dir, dirs, files in os.walk(one_path): + for file_name in files: + # Ignore existing PAR2 files + if file_name.lower().endswith('.par2'): + continue + + # Save filename and sub-directory + file_path = os.path.join(os.path.relpath(cur_dir, one_path), file_name) + if file_path.startswith('.\\'): + file_path = os.path.basename(file_path) + + #print("file name=", file_path) + f.write(file_path) + f.write('\n') + file_count += 1 + + # If number of source files reaches 1000, create PAR file for them. + if file_count >= 1000: + f.close() + #print("file_count=", file_count) + par_path = one_path + "\\#" + str(group_index) + ".par2" + + # Set command-line + # Cover path by " for possible space + # Specify source file by file-list + cmd = "\"" + client_path + "\" c " + par_option + " /d\"" + one_path + "\" /fu \"" + par_path + "\" \"" + list_path + "\"" + + # Process the command + print("Creating PAR files for group:", group_index) + error_level = command(cmd) + + # Check error + # Exit loop, when error occur. + if error_level > 0: + print("Error=", error_level) + break + + # Set for next group + group_index += 1 + file_count = 0 + f = open(list_path, 'w', encoding='utf-8') + + # Exit loop, when error occur. + if error_level > 0: + break + + # Finish file-list + f.close() + + # If there are source files still, create the last PAR file. + #print("file_count=", file_count) + if file_count > 0: + par_path = one_path + "\\#" + str(group_index) + ".par2" + cmd = "\"" + client_path + "\" c " + par_option + " /d\"" + one_path + "\" /fu \"" + par_path + "\" \"" + list_path + "\"" + + # Process the command + print("Creating PAR files for group:", group_index) + error_level = command(cmd) + + # Check error + # Exit loop, when error occur. + if error_level > 0: + print("Error=", error_level) + break + + elif group_index == 1: + print(one_name + " doesn't contain source files.") + + # Delete file-list after creation + if (group_index > 1) or (file_count > 0): + os.remove(list_path) + +# If you don't confirm result, comment out below line. +input('Press [Enter] key to continue . . .') diff --git a/alpha/tool/large_files.py b/alpha/tool/large_files.py new file mode 100644 index 0000000..db68f62 --- /dev/null +++ b/alpha/tool/large_files.py @@ -0,0 +1,111 @@ +import sys +import os +import subprocess + + +# Set path of MultiPar +client_path = "../MultiPar.exe" + +# Set path of file-list +list_path = "../save/file-list.txt" + + +# Make file-list of source files in a folder +# Return number of found files +def make_list(path): + f = open(list_path, 'w', encoding='utf-8') + file_count = 0 + + # Search inner files + with os.scandir(path) as it: + for entry in it: + # Exclude sub-folder and short-cut + if entry.is_file() and (not entry.name.endswith('lnk')): + #print("file name=", entry.name) + #print("file size=", entry.stat().st_size) + + # Check file size and ignore small files + # Set the limit number (bytes) on below line. + if entry.stat().st_size >= 1048576: + f.write(entry.name) + f.write('\n') + file_count += 1 + + # Finish file-list + f.close() + return file_count + + +# Return sub-process's ExitCode +def command(cmd): + ret = subprocess.run(cmd, shell=True) + return ret.returncode + + +# 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(): + total += entry.stat().st_size + elif entry.is_dir(): + total += check_empty(entry.path) + if total > 0: + break + return total + + +# Read arguments of command-line +for idx, arg in enumerate(sys.argv[1:]): + one_path = arg + one_name = os.path.basename(one_path) + + # Check the folder exists + if os.path.isdir(one_path) == False: + print(one_name + " isn't folder.") + continue + + # Check empty folder + if check_empty(one_path) == 0: + print(one_name + " is empty folder.") + continue + + print(one_name + " is folder.") + + # Path of creating PAR file + par_path = one_path + "\\" + one_name + ".par2" + + # Check the PAR file exists already + # You must check MultiPar Option: "Always use folder name for base filename". + if os.path.exists(par_path): + print(one_name + " includes PAR file already.") + continue + + # Make file-list + file_count = make_list(one_path) + #print("file_count=", file_count) + if file_count > 0: + + # Set command-line + # Cover path by " for possible space + # Specify source file by file-list + # The file-list will be deleted by MultiPar automatically. + cmd = "\"" + client_path + "\" /create /base \"" + one_path + "\" /list \"" + list_path + "\"" + + # Process the command + print("Creating PAR files.") + error_level = command(cmd) + + # Check error + # Exit loop, when error occur. + if error_level > 0: + print("Error=", error_level) + break + + else: + print(one_name + " doesn't contain source files.") + os.remove(list_path) + +# If you don't confirm result, comment out below line. +input('Press [Enter] key to continue . . .') diff --git a/alpha/tool/par2_rename.py b/alpha/tool/par2_rename.py new file mode 100644 index 0000000..066c43c --- /dev/null +++ b/alpha/tool/par2_rename.py @@ -0,0 +1,468 @@ +import os +import re +import sys +import glob +import struct +import hashlib +import tkinter as tk +from tkinter import ttk +from tkinter import filedialog + + +# Initialize global variables +folder_path = "./" +set_id = None +old_name = [] + + +# Search PAR files of same base name +def search_par_file(file_path): + global folder_path + + # Check file type by filename extension + file_name = os.path.basename(file_path) + file_base, file_ext = os.path.splitext(file_name) + if file_ext.lower() != '.par2': + label_status.config(text= "Selected one isn't PAR2 file.") + return + + # Save directory of selected file + #print("path=", file_path) + folder_path = os.path.dirname(file_path) + #print("path=", folder_path) + if folder_path == "": + folder_path = '.' + + # Clear list of PAR files + old_name.clear() + listbox_list1.delete(0, tk.END) + listbox_list2.delete(0, tk.END) + button_name.config(state=tk.DISABLED) + button_save.config(state=tk.DISABLED) + + # Compare filename in case insensitive + base_name = file_base.lower() + # Remove ".vol#-#", ".vol#+#", or ".vol_#" at the last + base_name = re.sub(r'[.]vol\d*[-+_]\d+$', "", base_name) + #print("base=", base_name) + listbox_list1.insert(tk.END, file_name) + file_count = 1 + + # Search other PAR2 files of same base name + for another_path in glob.glob(glob.escape(folder_path + "/" + base_name) + "*.par2"): + #print("path=", another_path) + another_name = os.path.basename(another_path) + if another_name == file_name: + continue + listbox_list1.insert(tk.END, another_name) + file_count += 1 + + # Ready to read PAR2 files + button_read.config(state=tk.NORMAL) + label_status.config(text= "There are " + str(file_count) + " PAR2 files of same base name.") + + +# Select file to search other PAR files +def button_file_clicked(): + global folder_path + + # Show file selecting dialog + file_type = [("PAR2 file", "*.par2"), ('All files', '*')] + file_path = filedialog.askopenfilename(filetypes=file_type, initialdir=folder_path, title="Select a PAR2 file to search others") + if file_path == "": + return + search_par_file(file_path) + + +# Read filenames in a PAR file +def button_read_clicked(): + global folder_path, set_id + + # Clear list of source files + set_id = None + old_name.clear() + listbox_list2.delete(0, tk.END) + + # Get name of a selected file. + # If not selected, get the first file. + indices = listbox_list1.curselection() + if len(indices) == 1: + file_name = listbox_list1.get(indices[0]) + else: + file_name = listbox_list1.get(0) + label_status.config(text= "Reading " + file_name + " ...") + + + # Open the selected PAR file and read the first 2 MB. + # Main packet and File Description packet would be smaller than 1 MB. + f = open(folder_path + "/" + file_name, 'rb') + data = f.read(2097152) + data_size = len(data) + + # Initialize + file_count = 0 + count_now = 0 + + # Search PAR2 packets + offset = 0 + while offset + 64 < data_size: + if data[offset : offset + 8] == b'PAR2\x00PKT': + # Packet size + packet_size = struct.unpack_from('Q', data, offset + 8)[0] + #print("size=", packet_size) + + # If a packet is larger than buffer size, just ignore it. + if offset + packet_size > data_size: + offset += 8 + continue + + # Checksum of the packet + hash = hashlib.md5(data[offset + 32 : offset + packet_size]).digest() + #print(hash) + if data[offset + 16 : offset + 32] == hash: + #print("MD5 is same.") + + # Set ID + if set_id == None: + set_id = data[offset + 32 : offset + 48] + #print("Set ID=", set_id) + elif set_id != data[offset + 32 : offset + 48]: + #print("Set ID is different.") + offset += packet_size + continue + + # Packet type + if data[offset + 48 : offset + 64] == b'PAR 2.0\x00Main\x00\x00\x00\x00': + #print("Main packet") + file_count = struct.unpack_from('I', data, offset + 72)[0] + #print("Number of source files=", file_count) + if file_count == 0: + break + elif count_now == file_count: + break + + elif data[offset + 48 : offset + 64] == b'PAR 2.0\x00FileDesc': + #print("File Description packet") + # Remove padding null bytes + name_end = packet_size + while data[offset + name_end - 1] == 0: + name_end -= 1 + #print("Filename length=", name_end - 64 - 56) + file_name = data[offset + 120 : offset + name_end].decode("UTF-8") + #print("Filename=", file_name) + # Ignore same name, if the name exists in the list already. + if file_name in old_name: + offset += packet_size + continue + old_name.append(file_name) + listbox_list2.insert(tk.END, file_name) + count_now += 1 + if count_now == file_count: + break + + offset += packet_size + else: + offset += 8 + else: + offset += 1 + + # When it reaches to half, read next 1 MB from the PAR file. + if offset >= 1048576: + #print("data_size=", data_size, "offset=", offset) + data = data[offset : data_size] + f.read(1048576) + data_size = len(data) + offset = 0 + + # Close file + f.close() + + if file_count > 0: + button_name.config(state=tk.NORMAL) + else: + button_name.config(state=tk.DISABLED) + button_save.config(state=tk.DISABLED) + label_status.config(text= "The PAR2 file includes " + str(file_count) + " source files.") + + +# Edit a name of a source file +def button_name_clicked(): + # Get current name of renaming file. + # If not selected, show error message. + indices = listbox_list2.curselection() + if len(indices) == 1: + current_name = listbox_list2.get(indices[0]) + else: + label_status.config(text= "Select a source file to rename.") + return + + # Get new name and check invalid characters + new_name = entry_edit.get() + if new_name == "": + label_status.config(text= "Enter new name in text box.") + return + elif new_name == current_name: + label_status.config(text= "New name is same as old one.") + return + elif "'" + new_name + "'" in s_list2.get(): + label_status.config(text= "New name is same as another filename.") + return + elif '\\' in new_name: + label_status.config(text= "Filename cannot include \\.") + return + elif ':' in new_name: + label_status.config(text= "Filename cannot include :.") + return + elif '*' in new_name: + label_status.config(text= "Filename cannot include *.") + return + elif '?' in new_name: + label_status.config(text= "Filename cannot include ?.") + return + elif '\"' in new_name: + label_status.config(text= "Filename cannot include \".") + return + elif '<' in new_name: + label_status.config(text= "Filename cannot include <.") + return + elif '>' in new_name: + label_status.config(text= "Filename cannot include >.") + return + elif '|' in new_name: + label_status.config(text= "Filename cannot include |.") + return + + # Add new name and remove old one + index = indices[0] + listbox_list2.insert(index + 1, new_name) + listbox_list2.delete(index) + label_status.config(text= "Renamed from \"" + current_name + "\" to \"" + new_name + "\"") + + # Only when names are changed, it's possible to save. + index = 0 + for name_one in old_name: + if listbox_list2.get(index) != name_one: + index = -1 + break + index += 1 + if index < 0: + button_save.config(state=tk.NORMAL) + else: + button_save.config(state=tk.DISABLED) + + +# Save as new PAR files +def button_save_clicked(): + global folder_path, set_id + + # Allocate 64 KB buffer for File Description packet + buffer = bytearray(65536) + rename_count = 0 + + # For each PAR file + file_index = 0 + file_count = listbox_list1.size() + while file_index < file_count: + file_name = listbox_list1.get(file_index) + file_index += 1 + #print("Target=", file_name) + + # Open a PAR file + fr = open(folder_path + "/" + file_name, 'rb') + data = fr.read(2097152) + data_size = len(data) + + # Name of new PAR files have prefix "new_". + fw = open(folder_path + "/new_" + file_name, 'wb') + + # Search PAR2 packets + offset = 0 + while offset + 64 < data_size: + if data[offset : offset + 8] == b'PAR2\x00PKT': + # Packet size + packet_size = struct.unpack_from('Q', data, offset + 8)[0] + #print("size=", packet_size) + + # If a packet is larger than buffer size, just ignore it. + if offset + packet_size > data_size: + fw.write(data[offset : offset + 8]) + offset += 8 + continue + + # Checksum of the packet + hash = hashlib.md5(data[offset + 32 : offset + packet_size]).digest() + #print(hash) + if data[offset + 16 : offset + 32] == hash: + #print("MD5 is same.") + + # Set ID + if set_id != data[offset + 32 : offset + 48]: + #print("Set ID is different.") + fw.write(data[offset : offset + packet_size]) + offset += packet_size + continue + + # Packet type + index = -1 + if data[offset + 48 : offset + 64] == b'PAR 2.0\x00FileDesc': + #print("File Description packet") + # Remove padding null bytes + name_end = packet_size + while data[offset + name_end - 1] == 0: + name_end -= 1 + #print("Filename length=", name_end - 64 - 56) + name_one = data[offset + 120 : offset + name_end].decode("UTF-8") + #print("Filename=", name_one) + if name_one in old_name: + index = old_name.index(name_one) + #print("index=", index) + new_name = listbox_list2.get(index) + if new_name != name_one: + # Copy from old packet + buffer[0 : 120] = data[offset : offset + 120] + + # Set new name + #print("New name=", new_name, "old name=", name_one) + name_byte = new_name.encode("UTF-8") + name_len = len(name_byte) + #print("byte=", name_byte, "len=", name_len) + buffer[120 : 120 + name_len] = name_byte + + # Padding null bytes + while name_len % 4 != 0: + buffer[120 + name_len] = 0 + name_len += 1 + #print("padded len=", name_len) + + # Update packet size + size_byte = struct.pack('Q', 120 + name_len) + buffer[8 : 16] = size_byte + #print("packet=", buffer[0 : 120 + name_len]) + + # Update checksum of packet + hash = hashlib.md5(buffer[32 : 120 + name_len]).digest() + buffer[16 : 32] = hash + + # Write new packet + fw.write(buffer[0 : 120 + name_len]) + rename_count += 1 + + # When filename isn't changed. + else: + index = -2 + + # Write packet with current data + if index < 0: + fw.write(data[offset : offset + packet_size]) + + offset += packet_size + else: + fw.write(data[offset : offset + 8]) + offset += 8 + else: + fw.write(data[offset : offset + 1]) + offset += 1 + + # When it reaches to half, read next. + if offset >= 1048576: + #print("data_size=", data_size, "offset=", offset) + data = data[offset : data_size] + fr.read(1048576) + data_size = len(data) + offset = 0 + + # Close file + fr.close() + fw.close() + + label_status.config(text= "Modified " + str(rename_count) + " packets in " + str(file_count) + " PAR2 files.") + + +# Window size and title +root = tk.Tk() +root.title('PAR2 Rename') +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(0, weight=1) + +# Body +frame_body = ttk.Frame(root, padding=(2,6,2,2)) +frame_body.grid(row=0, column=0, sticky=(tk.E,tk.W,tk.S,tk.N)) +frame_body.rowconfigure(0, weight=1) +frame_body.columnconfigure(0, weight=1) +frame_body.columnconfigure(1, weight=1) + +# List of PAR files +frame_list1 = ttk.Frame(frame_body, 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(1, 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_file = ttk.Button(frame_top1, text="File", width=9, command=button_file_clicked) +button_file.pack(side=tk.LEFT, padx=2) + +button_read = ttk.Button(frame_top1, text="Read", width=9, command=button_read_clicked, state=tk.DISABLED) +button_read.pack(side=tk.LEFT, padx=2) + +button_save = ttk.Button(frame_top1, text="Save", width=9, command=button_save_clicked, state=tk.DISABLED) +button_save.pack(side=tk.LEFT, padx=2) + +s_list1 = tk.StringVar() +listbox_list1 = tk.Listbox(frame_list1, listvariable=s_list1, activestyle=tk.NONE) +listbox_list1.grid(row=1, 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=1, column=1, sticky=(tk.N, tk.S)) +listbox_list1["yscrollcommand"] = scrollbar_list1.set + +# List of source files +frame_list2 = ttk.Frame(frame_body, 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(1, 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)) + +s_edit2 = tk.StringVar() +entry_edit = ttk.Entry(frame_top2,textvariable=s_edit2) +entry_edit.grid(row=0, column=0, padx=2, sticky=(tk.E,tk.W)) +frame_top2.columnconfigure(0, weight=1) + +button_name = ttk.Button(frame_top2, text="Rename", width=9, command=button_name_clicked, state=tk.DISABLED) +button_name.grid(row=0, column=1, padx=2) + +s_list2 = tk.StringVar() +listbox_list2 = tk.Listbox(frame_list2, listvariable=s_list2, activestyle=tk.NONE) +listbox_list2.grid(row=1, 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=1, column=1, sticky=(tk.N, tk.S)) +listbox_list2["yscrollcommand"] = scrollbar_list2.set + +# Status text +frame_foot = ttk.Frame(root) +frame_foot.grid(row=1, column=0, sticky=(tk.E,tk.W)) + +label_status = ttk.Label(frame_foot, text='Select a PAR2 file to rename included files.', width=100) +label_status.pack(side=tk.LEFT, padx=2) + + +# When file is specified in command-line +if len(sys.argv) > 1: + file_path = sys.argv[1] + if os.path.isfile(file_path): + #file_path = os.path.abspath(file_path) + search_par_file(file_path) + + +# Show window +root.mainloop() diff --git a/alpha/tool/queue_create.py b/alpha/tool/queue_create.py new file mode 100644 index 0000000..78bc5d2 --- /dev/null +++ b/alpha/tool/queue_create.py @@ -0,0 +1,703 @@ +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() diff --git a/alpha/tool/queue_verify.py b/alpha/tool/queue_verify.py new file mode 100644 index 0000000..8560ec3 --- /dev/null +++ b/alpha/tool/queue_verify.py @@ -0,0 +1,400 @@ +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 MultiPar +client_path = "../par2j64.exe" +gui_path = "../MultiPar.exe" +save_path = "../save" + + +# Initialize global variables +folder_path = "" +sub_proc = None + + +# Search PAR2 sets in a folder +def search_par_set(one_path): + if one_path == "": + return + + # Clear list-box at first + listbox_list1.delete(0, tk.END) + listbox_list2.delete(0, tk.END) + listbox_list3.delete(0, tk.END) + button_start.config(state=tk.DISABLED) + + # Add found PAR sets + for par_path in glob.glob(glob.escape(one_path) + "/*.par2"): + # Remove extension ".par2" + one_name = os.path.splitext( os.path.basename(par_path) )[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) + # Ignore same base, if the name exists in the list already. + if "'" + base_name + "'" in s_list1.get(): + continue + listbox_list1.insert(tk.END, base_name) + + + # Add found PAR sets in sub-directories + # This searches 1 level child only, because recursive search may be slow. + for par_path in glob.glob(glob.escape(one_path) + "/*/*.par2"): + # If you want to search recursively, use below line instead of above line. + #for par_path in glob.glob(glob.escape(one_path) + "/**/*.par2", recursive=True): + # Get relative path and convert to UNIX style directory mark + rel_path = os.path.relpath(par_path, one_path) + rel_path = rel_path.replace('\\', '/') + # Remove extension ".par2" + one_name = os.path.splitext(rel_path)[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) + # Ignore same base, if the name exists in the list already. + if "'" + base_name + "'" in s_list1.get(): + continue + listbox_list1.insert(tk.END, base_name) + + item_count = listbox_list1.size() + one_name = os.path.basename(one_path) + label_head1.config(text= str(item_count) + " sets in " + one_name) + if item_count == 0: + label_status.config(text= "There are no PAR sets in \"" + one_path + "\".") + else: + button_folder.config(state=tk.DISABLED) + button_open2.config(state=tk.DISABLED) + button_open3.config(state=tk.DISABLED) + # If you want to start manually, use these lines instead of below lines. + #label_status.config(text= str(item_count) + " sets were found in \"" + one_path + "\".") + button_start.config(state=tk.NORMAL) + # If you want to start verification automatically, use these lines instead of above lines. + #button_stop.config(state=tk.NORMAL) + #root.after(100, queue_run) + + +# Select folder to search PAR files +def button_folder_clicked(): + global folder_path + if folder_path == "": + s_initialdir = "./" + else: + s_initialdir = os.path.dirname(folder_path) + folder_path = filedialog.askdirectory(initialdir=s_initialdir) + if folder_path == "": + return + search_par_set(folder_path) + + +# Verify the first PAR set +def queue_run(): + global folder_path, sub_proc + + if sub_proc != None: + return + + if "disabled" in button_stop.state(): + button_folder.config(state=tk.NORMAL) + button_start.config(state=tk.NORMAL) + button_open2.config(state=tk.NORMAL) + button_open3.config(state=tk.NORMAL) + label_status.config(text= "Stopped queue") + return + + if folder_path == "": + label_status.config(text= "Select a folder at first.") + return + base_name = listbox_list1.get(0) + if base_name == "": + label_status.config(text= "There are no PAR sets.") + return + + one_path = folder_path + "\\" + base_name + ".par2" + label_status.config(text= "Verifying " + base_name) + + # Set command-line + # Cover path by " for possible space + cmd = "\"" + client_path + "\" v /fo /vs2 /vd\"" + save_path + "\" \"" + one_path + "\"" + # If you want to repair a damaged set automatically, use "r" command instead of "v". + #print(cmd) + + # Run PAR2 client + sub_proc = subprocess.Popen(cmd, shell=True) + + # Wait finish of verification + root.after(300, queue_result) + + +# Wait and read verification result +def queue_result(): + global folder_path, 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 + base_name = listbox_list1.get(0) + + # When all source files are complete + if (exit_code == 0) or (exit_code == 256): + # Add to list of complete set + listbox_list3.insert(tk.END, base_name) + item_count = listbox_list3.size() + label_head3.config(text= str(item_count) + " complete sets") + + # When fatal error happened in par2j + elif exit_code == 1: + button_folder.config(state=tk.NORMAL) + button_stop.config(state=tk.DISABLED) + button_open2.config(state=tk.NORMAL) + button_open3.config(state=tk.NORMAL) + label_status.config(text= "Failed queue") + return + + # When you cancel par2j on Command Prompt + elif exit_code == 2: + button_folder.config(state=tk.NORMAL) + button_start.config(state=tk.NORMAL) + button_stop.config(state=tk.DISABLED) + button_open2.config(state=tk.NORMAL) + button_open3.config(state=tk.NORMAL) + label_status.config(text= "Canceled queue") + return + + # When source files are bad + else: + #print("exit code =", exit_code) + # Add to list of bad set + listbox_list2.insert(tk.END, base_name) + item_count = listbox_list2.size() + label_head2.config(text= str(item_count) + " bad sets") + + # Remove the first item from the list + listbox_list1.delete(0) + + # Process next set + item_count = listbox_list1.size() + if item_count == 0: + button_folder.config(state=tk.NORMAL) + button_stop.config(state=tk.DISABLED) + button_open2.config(state=tk.NORMAL) + button_open3.config(state=tk.NORMAL) + label_status.config(text= "Verified all PAR sets") + + elif "disabled" in button_stop.state(): + button_folder.config(state=tk.NORMAL) + button_start.config(state=tk.NORMAL) + button_open2.config(state=tk.NORMAL) + button_open3.config(state=tk.NORMAL) + label_status.config(text= "Interrupted queue") + + else: + root.after(100, queue_run) + + +# Resume stopped queue +def button_start_clicked(): + global sub_proc + + button_folder.config(state=tk.DISABLED) + button_start.config(state=tk.DISABLED) + button_stop.config(state=tk.NORMAL) + button_open2.config(state=tk.DISABLED) + button_open3.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") + + +# 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: + base_name = listbox_list2.get(indices[0]) + one_path = folder_path + "\\" + base_name + ".par2" + # Set command-line + # Cover path by " for possible space + cmd = "\"" + gui_path + "\" /verify \"" + one_path + "\"" + + # Open MultiPar GUI to see details + # Because this doesn't wait finish of MultiPar, you may open some at once. + subprocess.Popen(cmd) + + +def button_open3_clicked(): + if os.path.exists(gui_path) == False: + label_status.config(text= "Cannot call \"" + gui_path + "\". Set path correctly.") + return + + indices = listbox_list3.curselection() + if len(indices) == 1: + base_name = listbox_list3.get(indices[0]) + one_path = folder_path + "\\" + base_name + ".par2" + # Set command-line + # Cover path by " for possible space + cmd = "\"" + gui_path + "\" /verify \"" + one_path + "\"" + + # Open MultiPar GUI to see details + # Because this doesn't wait finish of MultiPar, you may open some at once. + subprocess.Popen(cmd) + + +# Window size and title +root = tk.Tk() +root.title('PAR Queue - Verify') +root.minsize(width=520, 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(0, weight=1) + +# List +frame_middle = ttk.Frame(root, padding=(2,6,2,2)) +frame_middle.grid(row=0, 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) +frame_middle.columnconfigure(2, weight=1) + +# List of PAR 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_folder = ttk.Button(frame_top1, text="Folder", width=7, command=button_folder_clicked) +button_folder.pack(side=tk.LEFT, padx=2) + +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) + +label_head1 = ttk.Label(frame_list1, text='? sets in a folder') +label_head1.grid(row=1, column=0, columnspan=2) + +s_list1 = tk.StringVar() +listbox_list1 = tk.Listbox(frame_list1, listvariable=s_list1, activestyle='none') +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 bad files +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", command=button_open2_clicked, state=tk.DISABLED) +button_open2.pack(side=tk.LEFT, padx=2) + +label_head2 = ttk.Label(frame_list2, text='0 bad sets') +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 + +# List of complete files +frame_list3 = ttk.Frame(frame_middle, padding=(6,2,6,6), relief='groove') +frame_list3.grid(row=0, column=2, padx=4, sticky=(tk.E,tk.W,tk.S,tk.N)) +frame_list3.columnconfigure(0, weight=1) +frame_list3.rowconfigure(2, weight=1) + +frame_top3 = ttk.Frame(frame_list3, padding=(0,4,0,3)) +frame_top3.grid(row=0, column=0, columnspan=2, sticky=(tk.E,tk.W)) + +button_open3 = ttk.Button(frame_top3, text="Open with MultiPar", command=button_open3_clicked, state=tk.DISABLED) +button_open3.pack(side=tk.LEFT, padx=2) + +label_head3 = ttk.Label(frame_list3, text='0 complete sets') +label_head3.grid(row=1, column=0, columnspan=2) + +s_list3 = tk.StringVar() +listbox_list3 = tk.Listbox(frame_list3, listvariable=s_list3, activestyle='none') +listbox_list3.grid(row=2, column=0, sticky=(tk.E,tk.W,tk.S,tk.N)) + +scrollbar_list3 = ttk.Scrollbar(frame_list3, orient=tk.VERTICAL, command=listbox_list3.yview) +scrollbar_list3.grid(row=2, column=1, sticky=(tk.N, tk.S)) +listbox_list3["yscrollcommand"] = scrollbar_list3.set + +xscrollbar_list3 = ttk.Scrollbar(frame_list3, orient=tk.HORIZONTAL, command=listbox_list3.xview) +xscrollbar_list3.grid(row=3, column=0, sticky=(tk.E, tk.W)) +listbox_list3["xscrollcommand"] = xscrollbar_list3.set + +# Status text +frame_bottom = ttk.Frame(root) +frame_bottom.grid(row=1, column=0, sticky=(tk.E,tk.W)) + +label_status = ttk.Label(frame_bottom, text='Select a folder to search PAR files.') +label_status.pack(side=tk.LEFT, padx=2) + + +# When a folder is specified in command-line +if len(sys.argv) > 1: + folder_path = sys.argv[1] + if os.path.isdir(folder_path): + folder_name = os.path.basename(folder_path) + label_head1.config(text="? sets in " + folder_name) + else: + label_status.config(text= "\"" + folder_path + "\" isn't a folder.") + folder_path = "" + search_par_set(folder_path) + + +# Show window +root.mainloop() diff --git a/alpha/tool/read_json.py b/alpha/tool/read_json.py new file mode 100644 index 0000000..e79c4d1 --- /dev/null +++ b/alpha/tool/read_json.py @@ -0,0 +1,75 @@ +import sys +import os +import json + +# Get path of JSON file from command-line. +json_path = sys.argv[1] +print("JSON file = " + json_path) + +# Open the JSON file and read the contents. +with open(json_path, 'r', encoding='utf-8') as f: + json_dict = json.load(f) + + # Get directory of recovery files. + file_path = json_dict["SelectedFile"] + recv_dir = os.path.dirname(file_path) + print("\nRecovery files' directory = " + recv_dir) + + # Get list of recovery files. + recv_list = json_dict["RecoveryFile"] + for file_name in recv_list: + print(file_name) + + # Get directory of source files. + src_dir = json_dict["BaseDirectory"] + print("\nSource files' directory = " + src_dir) + + # Get list of source files. + src_list = json_dict["SourceFile"] + for file_name in src_list: + print(file_name) + + # Get list of found source files. + if "FoundFile" in json_dict: + find_list = json_dict["FoundFile"] + print("\nFound files =") + for file_name in find_list: + print(file_name) + + # Get list of external source files. + if "ExternalFile" in json_dict: + ext_list = json_dict["ExternalFile"] + print("\nExternal files =") + for file_name in ext_list: + print(file_name) + + # Get list of damaged source files. + if "DamagedFile" in json_dict: + damage_list = json_dict["DamagedFile"] + print("\nDamaged files =") + for file_name in damage_list: + print(file_name) + + # Get list of appended source files. + if "AppendedFile" in json_dict: + append_list = json_dict["AppendedFile"] + print("\nAppended files =") + for file_name in append_list: + print(file_name) + + # Get list of missing source files. + if "MissingFile" in json_dict: + miss_list = json_dict["MissingFile"] + print("\nMissing files =") + for file_name in miss_list: + print(file_name) + + # Get dict of misnamed source files. + if "MisnamedFile" in json_dict: + misname_dict = json_dict["MisnamedFile"] + print("\nMisnamed files =") + for file_names in misname_dict.items(): + print(file_names[0] + ", wrong name = " + file_names[1]) + +# If you don't confirm result, comment out below line. +input('Press [Enter] key to continue . . .')