Duplicate File Finder -Here's how to find those duplicated files, python script available in this post
Over time, our computers can collect a lot of duplicate files. Photos copied into different folders, old video backups, or documents saved more than once can take up a surprising amount of storage.
Duplicate File Finder is a simple Windows tool that helps you find those duplicate files quickly. It checks the actual contents of files, so it can recognize duplicates even when they have different names.
How to use it
Using the tool is pretty simple. First, open DuplicateFileFinder.py with Python. Once the program starts, click Add Folder and choose the folder you want to check.
You can add more than one folder if you want to compare files between different locations. After that, click Start Scan and let the program check the files.
When the scan finishes, the results will show the duplicate groups, file sizes, file types, and their locations.
If you want to remove extra copies, select the files you don't need and click Move Selected to Recycle Bin. The tool keeps the files in the Windows Recycle Bin instead of permanently deleting them, so you have a chance to recover them if you change your mind.
You can also export the results to a CSV file if you want to keep a list of the duplicates.
A small tip
For your first scan, I recommend starting with a folder such as Downloads, Pictures, or Videos. This lets you see how the program works before scanning your entire drive.
It’s a simple little tool, but it can be useful when your storage is getting full and you want to clean things up without manually searching through hundreds or thousands of files.
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
import csv
import hashlib
import os
import queue
import threading
import tkinter as tk
from collections import defaultdict
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
APP_NAME = "Duplicate File Finder"
HASH_BUFFER_SIZE = 1024 * 1024
IMAGE_EXTENSIONS = {
".jpg", ".jpeg", ".png", ".gif", ".bmp",
".webp", ".tif", ".tiff", ".ico"
}
VIDEO_EXTENSIONS = {
".mp4", ".mkv", ".avi", ".mov", ".wmv",
".flv", ".webm", ".m4v", ".3gp", ".mpeg",
".mpg", ".ts"
}
class DuplicateFileFinder:
def __init__(self, root):
self.root = root
self.root.title(APP_NAME)
self.root.geometry("1100x720")
self.root.minsize(900, 600)
self.folders = []
self.files = []
self.duplicate_groups = []
self.selected_files = set()
self.item_to_path = {}
self.path_to_item = {}
self.scan_thread = None
self.stop_scan_event = threading.Event()
self.scan_queue = queue.Queue()
self.total_files = 0
self.scanned_files = 0
self.duplicate_count = 0
self.wasted_space = 0
self.preview_photo = None
self.build_ui()
self.process_scan_queue()
# ---------------------------------------------------------
# UI
# ---------------------------------------------------------
def build_ui(self):
style = ttk.Style()
try:
style.theme_use("vista")
except tk.TclError:
pass
style.configure(
"Title.TLabel",
font=("Segoe UI", 20, "bold")
)
style.configure(
"Subtitle.TLabel",
font=("Segoe UI", 10)
)
style.configure(
"Stat.TLabel",
font=("Segoe UI", 10, "bold")
)
# Main container
main = ttk.Frame(self.root, padding=14)
main.pack(fill="both", expand=True)
# Header
header = ttk.Frame(main)
header.pack(fill="x", pady=(0, 12))
ttk.Label(
header,
text=APP_NAME,
style="Title.TLabel"
).pack(anchor="w")
ttk.Label(
header,
text="Find identical files and safely remove unnecessary copies.",
style="Subtitle.TLabel"
).pack(anchor="w", pady=(3, 0))
# Folder controls
folder_frame = ttk.LabelFrame(
main,
text="Folders to scan",
padding=10
)
folder_frame.pack(fill="x", pady=(0, 10))
folder_list_frame = ttk.Frame(folder_frame)
folder_list_frame.pack(fill="x")
self.folder_listbox = tk.Listbox(
folder_list_frame,
height=4,
selectmode=tk.EXTENDED,
font=("Segoe UI", 9)
)
self.folder_listbox.pack(
side="left",
fill="both",
expand=True
)
folder_scroll = ttk.Scrollbar(
folder_list_frame,
orient="vertical",
command=self.folder_listbox.yview
)
folder_scroll.pack(side="right", fill="y")
self.folder_listbox.configure(
yscrollcommand=folder_scroll.set
)
folder_buttons = ttk.Frame(folder_frame)
folder_buttons.pack(fill="x", pady=(8, 0))
self.add_folder_button = ttk.Button(
folder_buttons,
text="Add Folder",
command=self.add_folder
)
self.add_folder_button.pack(side="left", padx=(0, 6))
self.remove_folder_button = ttk.Button(
folder_buttons,
text="Remove Selected",
command=self.remove_folder
)
self.remove_folder_button.pack(side="left", padx=(0, 6))
self.clear_folders_button = ttk.Button(
folder_buttons,
text="Clear",
command=self.clear_folders
)
self.clear_folders_button.pack(side="left")
# Scan options
options_frame = ttk.LabelFrame(
main,
text="Scan options",
padding=10
)
options_frame.pack(fill="x", pady=(0, 10))
self.include_hidden_var = tk.BooleanVar(value=False)
self.include_system_var = tk.BooleanVar(value=False)
ttk.Checkbutton(
options_frame,
text="Include hidden files",
variable=self.include_hidden_var
).pack(side="left", padx=(0, 18))
ttk.Checkbutton(
options_frame,
text="Include system files",
variable=self.include_system_var
).pack(side="left", padx=(0, 18))
ttk.Label(
options_frame,
text="Minimum file size:"
).pack(side="left", padx=(10, 5))
self.min_size_var = tk.StringVar(value="0")
min_size_entry = ttk.Entry(
options_frame,
textvariable=self.min_size_var,
width=10
)
min_size_entry.pack(side="left")
ttk.Label(
options_frame,
text="MB"
).pack(side="left", padx=(5, 0))
# Buttons
action_frame = ttk.Frame(main)
action_frame.pack(fill="x", pady=(0, 10))
self.scan_button = ttk.Button(
action_frame,
text="Start Scan",
command=self.start_scan
)
self.scan_button.pack(side="left", padx=(0, 6))
self.stop_button = ttk.Button(
action_frame,
text="Stop Scan",
command=self.stop_scan,
state="disabled"
)
self.stop_button.pack(side="left", padx=(0, 6))
self.select_all_button = ttk.Button(
action_frame,
text="Select All Duplicates",
command=self.select_all_duplicates,
state="disabled"
)
self.select_all_button.pack(side="left", padx=(0, 6))
self.clear_selection_button = ttk.Button(
action_frame,
text="Clear Selection",
command=self.clear_selection,
state="disabled"
)
self.clear_selection_button.pack(side="left", padx=(0, 6))
self.export_button = ttk.Button(
action_frame,
text="Export CSV",
command=self.export_csv,
state="disabled"
)
self.export_button.pack(side="right")
# Statistics
stats_frame = ttk.Frame(main)
stats_frame.pack(fill="x", pady=(0, 8))
self.files_label = ttk.Label(
stats_frame,
text="Files scanned: 0",
style="Stat.TLabel"
)
self.files_label.pack(side="left", padx=(0, 25))
self.duplicates_label = ttk.Label(
stats_frame,
text="Duplicates: 0",
style="Stat.TLabel"
)
self.duplicates_label.pack(side="left", padx=(0, 25))
self.groups_label = ttk.Label(
stats_frame,
text="Groups: 0",
style="Stat.TLabel"
)
self.groups_label.pack(side="left", padx=(0, 25))
self.space_label = ttk.Label(
stats_frame,
text="Wasted space: 0 B",
style="Stat.TLabel"
)
self.space_label.pack(side="left")
# Progress
self.progress = ttk.Progressbar(
main,
mode="determinate"
)
self.progress.pack(fill="x", pady=(0, 5))
self.status_label = ttk.Label(
main,
text="Ready."
)
self.status_label.pack(anchor="w", pady=(0, 8))
# Results
results_frame = ttk.LabelFrame(
main,
text="Duplicate files",
padding=8
)
results_frame.pack(fill="both", expand=True)
tree_container = ttk.Frame(results_frame)
tree_container.pack(fill="both", expand=True)
columns = (
"selected",
"name",
"size",
"type",
"path"
)
self.tree = ttk.Treeview(
tree_container,
columns=columns,
show="headings",
selectmode="browse"
)
self.tree.heading(
"selected",
text="✓"
)
self.tree.heading(
"name",
text="File"
)
self.tree.heading(
"size",
text="Size"
)
self.tree.heading(
"type",
text="Type"
)
self.tree.heading(
"path",
text="Location"
)
self.tree.column(
"selected",
width=45,
anchor="center",
stretch=False
)
self.tree.column(
"name",
width=220,
anchor="w"
)
self.tree.column(
"size",
width=100,
anchor="e"
)
self.tree.column(
"type",
width=90,
anchor="center"
)
self.tree.column(
"path",
width=550,
anchor="w"
)
tree_scroll_y = ttk.Scrollbar(
tree_container,
orient="vertical",
command=self.tree.yview
)
tree_scroll_x = ttk.Scrollbar(
tree_container,
orient="horizontal",
command=self.tree.xview
)
self.tree.configure(
yscrollcommand=tree_scroll_y.set,
xscrollcommand=tree_scroll_x.set
)
self.tree.grid(
row=0,
column=0,
sticky="nsew"
)
tree_scroll_y.grid(
row=0,
column=1,
sticky="ns"
)
tree_scroll_x.grid(
row=1,
column=0,
sticky="ew"
)
tree_container.rowconfigure(0, weight=1)
tree_container.columnconfigure(0, weight=1)
self.tree.bind(
"<Button-1>",
self.on_tree_click
)
self.tree.bind(
"<Double-1>",
self.on_tree_double_click
)
# Bottom controls
bottom_frame = ttk.Frame(main)
bottom_frame.pack(fill="x", pady=(10, 0))
self.selection_label = ttk.Label(
bottom_frame,
text="Selected: 0 files"
)
self.selection_label.pack(side="left")
self.delete_button = ttk.Button(
bottom_frame,
text="Move Selected to Recycle Bin",
command=self.delete_selected,
state="disabled"
)
self.delete_button.pack(side="right")
# ---------------------------------------------------------
# Folder handling
# ---------------------------------------------------------
def add_folder(self):
folder = filedialog.askdirectory(
title="Select folder to scan"
)
if not folder:
return
folder = os.path.abspath(folder)
if folder not in self.folders:
self.folders.append(folder)
self.folder_listbox.insert(
tk.END,
folder
)
def remove_folder(self):
selected = list(
self.folder_listbox.curselection()
)
if not selected:
return
for index in reversed(selected):
self.folder_listbox.delete(index)
del self.folders[index]
def clear_folders(self):
self.folders.clear()
self.folder_listbox.delete(
0,
tk.END
)
# ---------------------------------------------------------
# Scanning
# ---------------------------------------------------------
def start_scan(self):
if not self.folders:
messagebox.showwarning(
APP_NAME,
"Please add at least one folder to scan."
)
return
try:
minimum_mb = float(
self.min_size_var.get().strip()
)
if minimum_mb < 0:
raise ValueError
except ValueError:
messagebox.showerror(
APP_NAME,
"Minimum file size must be a valid number."
)
return
self.clear_results()
self.stop_scan_event.clear()
self.scan_button.configure(
state="disabled"
)
self.stop_button.configure(
state="normal"
)
self.select_all_button.configure(
state="disabled"
)
self.clear_selection_button.configure(
state="disabled"
)
self.delete_button.configure(
state="disabled"
)
self.export_button.configure(
state="disabled"
)
self.progress.configure(
value=0,
maximum=100
)
self.status_label.configure(
text="Preparing scan..."
)
self.scan_thread = threading.Thread(
target=self.scan_worker,
args=(minimum_mb,),
daemon=True
)
self.scan_thread.start()
def stop_scan(self):
if self.scan_thread and self.scan_thread.is_alive():
self.stop_scan_event.set()
self.status_label.configure(
text="Stopping scan..."
)
self.stop_button.configure(
state="disabled"
)
def scan_worker(self, minimum_mb):
try:
minimum_bytes = int(
minimum_mb * 1024 * 1024
)
self.scan_queue.put(
("status", "Collecting files...")
)
files = []
for folder in self.folders:
if self.stop_scan_event.is_set():
self.scan_queue.put(
("stopped", None)
)
return
self.collect_files(
folder,
files,
minimum_bytes
)
if self.stop_scan_event.is_set():
self.scan_queue.put(
("stopped", None)
)
return
self.files = files
self.scan_queue.put(
(
"total_files",
len(files)
)
)
self.scan_queue.put(
(
"status",
f"Found {len(files):,} files. Comparing sizes..."
)
)
size_groups = defaultdict(list)
for path in files:
if self.stop_scan_event.is_set():
self.scan_queue.put(
("stopped", None)
)
return
try:
size = os.path.getsize(path)
size_groups[size].append(path)
except OSError:
continue
candidates = []
for paths in size_groups.values():
if len(paths) > 1:
candidates.extend(paths)
self.scan_queue.put(
(
"candidate_count",
len(candidates)
)
)
if not candidates:
self.scan_queue.put(
(
"finished",
[]
)
)
return
self.scan_queue.put(
(
"status",
f"Hashing {len(candidates):,} possible duplicate files..."
)
)
hash_groups = defaultdict(list)
total_candidates = len(candidates)
for index, path in enumerate(candidates, 1):
if self.stop_scan_event.is_set():
self.scan_queue.put(
("stopped", None)
)
return
file_hash = self.calculate_hash(path)
if file_hash:
hash_groups[
(os.path.getsize(path), file_hash)
].append(path)
self.scan_queue.put(
(
"hash_progress",
(
index,
total_candidates,
path
)
)
)
duplicate_groups = []
for group in hash_groups.values():
if len(group) > 1:
group = sorted(
group,
key=lambda item: (
os.path.basename(item).lower(),
item.lower()
)
)
duplicate_groups.append(
group
)
duplicate_groups.sort(
key=lambda group: (
-os.path.getsize(group[0]),
group[0].lower()
)
)
self.scan_queue.put(
(
"finished",
duplicate_groups
)
)
except Exception as error:
self.scan_queue.put(
(
"error",
str(error)
)
)
def collect_files(
self,
folder,
result,
minimum_bytes
):
try:
for root, directories, filenames in os.walk(
folder,
topdown=True,
followlinks=False
):
if self.stop_scan_event.is_set():
return
# Remove inaccessible/system folders where possible.
filtered_directories = []
for directory in directories:
full_path = os.path.join(
root,
directory
)
try:
attributes = os.stat(
full_path
).st_file_attributes
hidden = bool(
attributes & 0x2
)
system = bool(
attributes & 0x4
)
if hidden and not self.include_hidden_var.get():
continue
if system and not self.include_system_var.get():
continue
except (OSError, AttributeError):
pass
filtered_directories.append(
directory
)
directories[:] = filtered_directories
for filename in filenames:
if self.stop_scan_event.is_set():
return
full_path = os.path.join(
root,
filename
)
try:
attributes = os.stat(
full_path
).st_file_attributes
hidden = bool(
attributes & 0x2
)
system = bool(
attributes & 0x4
)
if hidden and not self.include_hidden_var.get():
continue
if system and not self.include_system_var.get():
continue
except (OSError, AttributeError):
continue
try:
if not os.path.isfile(full_path):
continue
size = os.path.getsize(
full_path
)
if size < minimum_bytes:
continue
result.append(
os.path.abspath(full_path)
)
except OSError:
continue
except (OSError, PermissionError):
return
def calculate_hash(self, path):
sha256 = hashlib.sha256()
try:
with open(
path,
"rb"
) as file:
while True:
chunk = file.read(
HASH_BUFFER_SIZE
)
if not chunk:
break
sha256.update(
chunk
)
return sha256.hexdigest()
except (
OSError,
PermissionError
):
return None
# ---------------------------------------------------------
# Scan queue
# ---------------------------------------------------------
def process_scan_queue(self):
try:
while True:
event, data = self.scan_queue.get_nowait()
if event == "status":
self.status_label.configure(
text=data
)
elif event == "total_files":
self.total_files = data
self.files_label.configure(
text=f"Files found: {data:,}"
)
elif event == "candidate_count":
self.progress.configure(
maximum=max(data, 1),
value=0
)
elif event == "hash_progress":
index, total, path = data
self.progress.configure(
maximum=max(total, 1),
value=index
)
self.files_label.configure(
text=f"Hashing: {index:,} / {total:,}"
)
self.status_label.configure(
text=f"Checking: {os.path.basename(path)}"
)
elif event == "finished":
self.finish_scan(
data
)
elif event == "stopped":
self.scan_finished_stopped()
elif event == "error":
self.scan_error(
data
)
except queue.Empty:
pass
self.root.after(
100,
self.process_scan_queue
)
def finish_scan(self, groups):
self.duplicate_groups = groups
self.duplicate_count = sum(
len(group)
for group in groups
)
self.wasted_space = 0
for group in groups:
try:
size = os.path.getsize(
group[0]
)
self.wasted_space += (
size * (len(group) - 1)
)
except OSError:
pass
self.display_results()
self.scan_button.configure(
state="normal"
)
self.stop_button.configure(
state="disabled"
)
if groups:
self.select_all_button.configure(
state="normal"
)
self.clear_selection_button.configure(
state="normal"
)
self.export_button.configure(
state="normal"
)
self.status_label.configure(
text=(
f"Scan complete. "
f"{len(groups):,} duplicate groups found."
)
)
def scan_finished_stopped(self):
self.scan_button.configure(
state="normal"
)
self.stop_button.configure(
state="disabled"
)
self.status_label.configure(
text="Scan stopped."
)
def scan_error(self, error):
self.scan_button.configure(
state="normal"
)
self.stop_button.configure(
state="disabled"
)
self.status_label.configure(
text="Scan failed."
)
messagebox.showerror(
APP_NAME,
f"The scan encountered an error:\n\n{error}"
)
# ---------------------------------------------------------
# Results
# ---------------------------------------------------------
def clear_results(self):
for item in self.tree.get_children():
self.tree.delete(item)
self.files = []
self.duplicate_groups = []
self.selected_files.clear()
self.item_to_path.clear()
self.path_to_item.clear()
self.duplicate_count = 0
self.wasted_space = 0
self.files_label.configure(
text="Files scanned: 0"
)
self.duplicates_label.configure(
text="Duplicates: 0"
)
self.groups_label.configure(
text="Groups: 0"
)
self.space_label.configure(
text="Wasted space: 0 B"
)
self.selection_label.configure(
text="Selected: 0 files"
)
def display_results(self):
self.clear_tree_only()
for group_number, group in enumerate(
self.duplicate_groups,
1
):
size = 0
try:
size = os.path.getsize(
group[0]
)
except OSError:
pass
for duplicate_number, path in enumerate(
group,
1
):
item_id = self.tree.insert(
"",
"end",
values=(
"☐",
os.path.basename(path),
self.format_size(size),
self.get_file_type(path),
path
)
)
self.item_to_path[item_id] = path
self.path_to_item[path] = item_id
if duplicate_number == 1:
self.tree.item(
item_id,
tags=("original",)
)
self.tree.tag_configure(
"original",
font=("Segoe UI", 9, "bold")
)
self.duplicates_label.configure(
text=f"Duplicates: {self.duplicate_count:,}"
)
self.groups_label.configure(
text=f"Groups: {len(self.duplicate_groups):,}"
)
self.space_label.configure(
text=(
"Wasted space: "
f"{self.format_size(self.wasted_space)}"
)
)
def clear_tree_only(self):
for item in self.tree.get_children():
self.tree.delete(item)
self.item_to_path.clear()
self.path_to_item.clear()
self.selected_files.clear()
# ---------------------------------------------------------
# Selection
# ---------------------------------------------------------
def on_tree_click(self, event):
region = self.tree.identify(
"region",
event.x,
event.y
)
if region != "cell":
return
column = self.tree.identify_column(
event.x
)
if column != "#1":
return
item_id = self.tree.identify_row(
event.y
)
if not item_id:
return
self.toggle_item(
item_id
)
def toggle_item(self, item_id):
path = self.item_to_path.get(
item_id
)
if not path:
return
if path in self.selected_files:
self.selected_files.remove(
path
)
self.tree.set(
item_id,
"selected",
"☐"
)
else:
self.selected_files.add(
path
)
self.tree.set(
item_id,
"selected",
"☑"
)
self.update_selection_status()
def update_selection_status(self):
selected_size = 0
for path in self.selected_files:
try:
selected_size += os.path.getsize(
path
)
except OSError:
pass
self.selection_label.configure(
text=(
f"Selected: {len(self.selected_files):,} files"
f" | "
f"{self.format_size(selected_size)}"
)
)
if self.selected_files:
self.delete_button.configure(
state="normal"
)
else:
self.delete_button.configure(
state="disabled"
)
def select_all_duplicates(self):
self.selected_files.clear()
# Keep the first file in every duplicate group.
# Select all other copies.
for group in self.duplicate_groups:
for path in group[1:]:
self.selected_files.add(
path
)
self.refresh_checkboxes()
self.update_selection_status()
def clear_selection(self):
self.selected_files.clear()
self.refresh_checkboxes()
self.update_selection_status()
def refresh_checkboxes(self):
for item_id, path in self.item_to_path.items():
if path in self.selected_files:
self.tree.set(
item_id,
"selected",
"☑"
)
else:
self.tree.set(
item_id,
"selected",
"☐"
)
# ---------------------------------------------------------
# File actions
# ---------------------------------------------------------
def delete_selected(self):
if not self.selected_files:
return
selected = list(
self.selected_files
)
total_size = 0
for path in selected:
try:
total_size += os.path.getsize(
path
)
except OSError:
pass
answer = messagebox.askyesno(
APP_NAME,
(
f"Move {len(selected):,} selected file(s) "
f"to the Recycle Bin?\n\n"
f"Total size: {self.format_size(total_size)}\n\n"
"The files will not be permanently deleted."
)
)
if not answer:
return
moved = 0
failed = []
for path in selected:
try:
self.move_to_recycle_bin(
path
)
moved += 1
except Exception as error:
failed.append(
f"{path}\n{error}"
)
self.selected_files.clear()
self.remove_missing_results()
self.update_selection_status()
if failed:
messagebox.showwarning(
APP_NAME,
(
f"Moved {moved:,} file(s) to the Recycle Bin.\n\n"
f"{len(failed):,} file(s) could not be moved."
)
)
else:
messagebox.showinfo(
APP_NAME,
(
f"Moved {moved:,} file(s) to the Recycle Bin."
)
)
def move_to_recycle_bin(self, path):
# Windows Shell API.
if os.name != "nt":
raise RuntimeError(
"Recycle Bin support in this version is for Windows."
)
import ctypes
from ctypes import wintypes
class SHFILEOPSTRUCTW(
ctypes.Structure
):
_fields_ = [
("hwnd", wintypes.HWND),
("wFunc", ctypes.c_uint),
("pFrom", wintypes.LPCWSTR),
("pTo", wintypes.LPCWSTR),
("fFlags", ctypes.c_uint16),
("fAnyOperationsAborted", wintypes.BOOL),
("hNameMappings", wintypes.LPVOID),
("lpszProgressTitle", wintypes.LPCWSTR),
]
FO_DELETE = 0x0003
FOF_ALLOWUNDO = 0x0040
FOF_NOCONFIRMATION = 0x0010
FOF_SILENT = 0x0004
file_operation = SHFILEOPSTRUCTW()
file_operation.hwnd = self.root.winfo_id()
file_operation.wFunc = FO_DELETE
# Double null terminated.
file_operation.pFrom = (
path + "\0"
)
file_operation.pTo = None
file_operation.fFlags = (
FOF_ALLOWUNDO
| FOF_NOCONFIRMATION
| FOF_SILENT
)
result = ctypes.windll.shell32.SHFileOperationW(
ctypes.byref(file_operation)
)
if result != 0:
raise OSError(
f"Windows Shell error code: {result}"
)
if file_operation.fAnyOperationsAborted:
raise OSError(
"Operation was cancelled."
)
def remove_missing_results(self):
new_groups = []
for group in self.duplicate_groups:
remaining = []
for path in group:
if os.path.exists(path):
remaining.append(
path
)
if len(remaining) > 1:
new_groups.append(
remaining
)
self.duplicate_groups = new_groups
self.duplicate_count = sum(
len(group)
for group in new_groups
)
self.wasted_space = 0
for group in new_groups:
try:
size = os.path.getsize(
group[0]
)
self.wasted_space += (
size * (len(group) - 1)
)
except OSError:
pass
self.display_results()
if new_groups:
self.select_all_button.configure(
state="normal"
)
self.clear_selection_button.configure(
state="normal"
)
self.export_button.configure(
state="normal"
)
else:
self.select_all_button.configure(
state="disabled"
)
self.clear_selection_button.configure(
state="disabled"
)
self.export_button.configure(
state="disabled"
)
self.delete_button.configure(
state="disabled"
)
# ---------------------------------------------------------
# Preview / open
# ---------------------------------------------------------
def on_tree_double_click(self, event):
item_id = self.tree.identify_row(
event.y
)
if not item_id:
return
path = self.item_to_path.get(
item_id
)
if not path:
return
if not os.path.exists(path):
messagebox.showwarning(
APP_NAME,
"This file no longer exists."
)
return
self.open_file_location(
path
)
def open_file_location(self, path):
if os.name == "nt":
import subprocess
subprocess.Popen(
[
"explorer",
"/select,",
os.path.normpath(path)
]
)
elif os.name == "darwin":
import subprocess
subprocess.Popen(
[
"open",
"-R",
path
]
)
else:
import subprocess
subprocess.Popen(
[
"xdg-open",
os.path.dirname(path)
]
)
# ---------------------------------------------------------
# CSV
# ---------------------------------------------------------
def export_csv(self):
if not self.duplicate_groups:
return
filename = filedialog.asksaveasfilename(
title="Export duplicate results",
defaultextension=".csv",
filetypes=[
(
"CSV files",
"*.csv"
),
(
"All files",
"*.*"
)
]
)
if not filename:
return
try:
with open(
filename,
"w",
newline="",
encoding="utf-8-sig"
) as file:
writer = csv.writer(
file
)
writer.writerow(
[
"Group",
"File",
"Size",
"Type",
"Path"
]
)
for group_number, group in enumerate(
self.duplicate_groups,
1
):
for path in group:
try:
size = os.path.getsize(
path
)
except OSError:
size = 0
writer.writerow(
[
group_number,
os.path.basename(path),
self.format_size(size),
self.get_file_type(path),
path
]
)
messagebox.showinfo(
APP_NAME,
f"Results exported successfully:\n\n{filename}"
)
except OSError as error:
messagebox.showerror(
APP_NAME,
f"Could not export the CSV file:\n\n{error}"
)
# ---------------------------------------------------------
# Helpers
# ---------------------------------------------------------
@staticmethod
def format_size(size):
units = [
"B",
"KB",
"MB",
"GB",
"TB",
"PB"
]
size = float(size)
for unit in units:
if size < 1024:
if unit == "B":
return f"{int(size):,} {unit}"
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} EB"
@staticmethod
def get_file_type(path):
extension = Path(path).suffix.lower()
if extension in IMAGE_EXTENSIONS:
return "Image"
if extension in VIDEO_EXTENSIONS:
return "Video"
if extension:
return extension[1:].upper()
return "File"
def main():
root = tk.Tk()
app = DuplicateFileFinder(
root
)
root.mainloop()
if __name__ == "__main__":
main()
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Comments
Post a Comment