Add files via upload
This commit is contained in:
428
alpha/tool/diff_folder.py
Normal file
428
alpha/tool/diff_folder.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user