Comments and cleanup; resuming this project
This commit is contained in:
parent
ce4eafdc98
commit
fafa69202b
5 changed files with 213 additions and 96 deletions
12
README.md
12
README.md
|
|
@ -4,7 +4,17 @@
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
A simple file sorter app, built with the Python TKinter library. This is my first real Python project, after many useless scripts and experiments.
|
A simple file sorter app, built with the Python TKinter library. It can automatically move, copy, or delete files from a given directory, based on file types. For example, maybe your Downloads folder is cluttered with a bunch of useless files, but you want to keep the image files by moving them into your Pictures folder. This app can help you move the images and delete the junk, all in one click (after a bit of setup).
|
||||||
|
|
||||||
|
Other use cases:
|
||||||
|
|
||||||
|
- Moving all video files out of your camera roll
|
||||||
|
|
||||||
|
- Copying only certain types of documents into a backup folder
|
||||||
|
|
||||||
|
- TODO: Grouping all of your photos into separate folders, by year
|
||||||
|
|
||||||
|
This is my first real Python project, after many unimportant scripts and experiments. I really needed a project like this in my portfolio, even if it's not truly amazing.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
||||||
238
app.py
238
app.py
|
|
@ -1,11 +1,17 @@
|
||||||
"""
|
"""
|
||||||
Frontend GUI that displays a list of files from a folder.
|
Frontend GUI that displays a list of files from a folder.
|
||||||
|
|
||||||
Also, there are various settings for different generic file types.
|
File actions (move, copy, or delete) can be set for generic file types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# Add comments
|
||||||
|
# Add some error handling
|
||||||
|
# Enable loading custom configuration json
|
||||||
|
# Improve user experience of rules tab
|
||||||
|
|
||||||
__name__ = "__main__"
|
__name__ = "__main__"
|
||||||
__version__ = "0.8"
|
__version__ = "0.9"
|
||||||
__author__ = "Gull"
|
__author__ = "Gull"
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
|
@ -16,19 +22,12 @@ from tkinter import ttk
|
||||||
from tkinter import messagebox as mb
|
from tkinter import messagebox as mb
|
||||||
from tkinter import filedialog as fd
|
from tkinter import filedialog as fd
|
||||||
|
|
||||||
debug_mode = False
|
|
||||||
APP_NAME = "Finch Filer"
|
APP_NAME = "Finch Filer"
|
||||||
LOG_PATH = "%/Gull/app.log"
|
LOG_PATH = "%/Gull/app.log"
|
||||||
RULES_PATH = "%/Gull/file_rules_custom.json"
|
RULES_PATH = "%/Gull/file_rules_custom.json"
|
||||||
|
|
||||||
def set_debug_mode(value):
|
|
||||||
global debug_mode
|
|
||||||
if type(value) == None:
|
|
||||||
debug_mode = not debug_mode # Toggles the mode
|
|
||||||
else:
|
|
||||||
debug_mode = value
|
|
||||||
|
|
||||||
def setup_log():
|
def setup_log():
|
||||||
|
"""Initializes the logging system."""
|
||||||
from sys import stdout
|
from sys import stdout
|
||||||
|
|
||||||
log.basicConfig(**ut.get_log_config(LOG_PATH, log.DEBUG))
|
log.basicConfig(**ut.get_log_config(LOG_PATH, log.DEBUG))
|
||||||
|
|
@ -36,27 +35,31 @@ def setup_log():
|
||||||
log.info("App started")
|
log.info("App started")
|
||||||
|
|
||||||
class App(tk.Tk):
|
class App(tk.Tk):
|
||||||
|
"""TKinter GUI and related methods."""
|
||||||
def __init__(self, width=640, height=480):
|
def __init__(self, width=640, height=480):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.title(APP_NAME)
|
self.title(APP_NAME)
|
||||||
self.current_mode = "all"
|
|
||||||
self.option_add('*tearOff', tk.FALSE)
|
self.option_add('*tearOff', tk.FALSE)
|
||||||
self.fm = fm.Manager(True)
|
self.fm = fm.Manager(True) # Main file manager object
|
||||||
self.log = list()
|
self.log = list() # Running list of all logs
|
||||||
self.gui()
|
self.temp = {"column": "", "reverse": False} # Temporary data
|
||||||
self.load_config()
|
self.current_mode = "all"
|
||||||
self.prep()
|
self.custom_rules_path = RULES_PATH
|
||||||
|
self.gui() # User interface
|
||||||
|
self.load_config() # File rules configuration
|
||||||
|
self.prep() # Ready state
|
||||||
|
|
||||||
|
# Centers the app on the screen
|
||||||
screen_width = self.winfo_screenwidth()
|
screen_width = self.winfo_screenwidth()
|
||||||
screen_height = self.winfo_screenheight()
|
screen_height = self.winfo_screenheight()
|
||||||
|
|
||||||
center_x = int(screen_width / 2 - width / 2)
|
center_x = int(screen_width / 2 - width / 2)
|
||||||
center_y = int(screen_height / 2 - height / 2)
|
center_y = int(screen_height / 2 - height / 2)
|
||||||
|
|
||||||
self.geometry(f"{width}x{height}+{center_x}+{center_y}")
|
self.geometry(f"{width}x{height}+{center_x}+{center_y}")
|
||||||
|
|
||||||
def get_filemode_files(self, key):
|
def get_filemode_files(self, key, ignore_all=True):
|
||||||
if key == "all": return
|
"""Returns a dict of files that match the given file mode key."""
|
||||||
|
if key == "all": # Special key representing all file types
|
||||||
|
return self.fm.filedata if not ignore_all else None
|
||||||
|
|
||||||
files = dict()
|
files = dict()
|
||||||
for k, v in self.fm.filedata.items():
|
for k, v in self.fm.filedata.items():
|
||||||
|
|
@ -65,66 +68,106 @@ class App(tk.Tk):
|
||||||
return files if len(files) > 0 else None
|
return files if len(files) > 0 else None
|
||||||
|
|
||||||
def update_fileview(self):
|
def update_fileview(self):
|
||||||
|
"""Updates the treeview widget in the app."""
|
||||||
tree = self.v_tree.get()
|
tree = self.v_tree.get()
|
||||||
self.fm.update_file_data()
|
self.fm.update_file_data()
|
||||||
log.info(f"Processed file view with {len(self.fm.filedata)} items")
|
log.info(f"Processed file view with {len(self.fm.filedata)} items")
|
||||||
log.info(f"Show as tree: {str(tree)}")
|
log.info(f"Show as tree: {str(tree)}")
|
||||||
|
|
||||||
while len(self.fileview.get_children()) > 0:
|
while len(self.fileview.get_children()) > 0: # Clears all children
|
||||||
self.fileview.delete(self.fileview.get_children()[-1])
|
self.fileview.delete(self.fileview.get_children()[-1])
|
||||||
|
|
||||||
if tree:
|
filemodes = self.fm.filemodes
|
||||||
for k, v in self.fm.filemodes.items():
|
if not tree: filemodes = {"all": {"name": "All Files"}} # Fallback
|
||||||
files = self.get_filemode_files(k)
|
for k, v in filemodes.items():
|
||||||
if files != None:
|
files = self.get_filemode_files(k, tree)
|
||||||
id = self.fileview.insert("", tk.END, text=v["name"],
|
if files != None:
|
||||||
open=True)
|
id = self.fileview.insert("", tk.END, text=v["name"],
|
||||||
|
open=True)
|
||||||
|
|
||||||
for k, v in files.items():
|
for i, (k, v) in enumerate(files.items()):
|
||||||
values = (str(ut.format_date(v["time"])),
|
values = (str(ut.format_date(v["time"])),
|
||||||
str(ut.format_bytes(v["size"])))
|
str(ut.format_bytes(v["size"])))
|
||||||
self.fileview.insert(id, tk.END, text=k, values=values)
|
nid = self.fileview.insert(id, tk.END, text=k,
|
||||||
|
values=values)
|
||||||
|
self.fm.filedata[k]["_treeid"] = nid
|
||||||
|
self.fm.filedata[k]["_order"] = i # Used for sorting
|
||||||
|
|
||||||
else:
|
def sort_fileview_items(self, column="name"):
|
||||||
for k, v in self.fm.filedata.items():
|
"""Sorts treeview items by the given column type."""
|
||||||
values = (str(ut.format_date(v["time"])),
|
columns = ("name", "time", "size")
|
||||||
str(ut.format_bytes(v["size"])))
|
c_index = columns.index(column) if column in columns else -1
|
||||||
self.fileview.insert("", tk.END, text=k, values=values)
|
if c_index == -1: return
|
||||||
|
|
||||||
def load_config(self, filepath=RULES_PATH):
|
reverse = False
|
||||||
|
if self.temp["column"] == column:
|
||||||
|
self.temp["reverse"] = not self.temp["reverse"] # Toggles reverse
|
||||||
|
reverse = self.temp["reverse"]
|
||||||
|
|
||||||
|
self.temp["column"] = column
|
||||||
|
|
||||||
|
files = self.fm.filedata
|
||||||
|
temp = list() # Stores file data in a temporary list, for sorting
|
||||||
|
for c1 in self.fileview.get_children():
|
||||||
|
for c2 in self.fileview.get_children(c1):
|
||||||
|
key = self.fileview.item(c2)["text"]
|
||||||
|
temp.append((key, files[key]["time"], files[key]["size"]))
|
||||||
|
temp.sort(key=lambda v: v[c_index], reverse=reverse)
|
||||||
|
|
||||||
|
for i, v in enumerate(temp):
|
||||||
|
files[v[0]]["_order"] = i
|
||||||
|
|
||||||
|
for index, (k, _, _) in enumerate(temp):
|
||||||
|
id = files[k]["_treeid"]
|
||||||
|
index = files[k]["_order"]
|
||||||
|
self.fileview.move(id, self.fileview.parent(id), index)
|
||||||
|
|
||||||
|
temp.clear() # Clears list after every loop
|
||||||
|
|
||||||
|
def load_config(self, filepath=""):
|
||||||
|
"""Loads the file sorting rules from the given file path."""
|
||||||
|
if filepath == "": filepath = self.custom_rules_path
|
||||||
path = ut.parse_dir(filepath)
|
path = ut.parse_dir(filepath)
|
||||||
data = ut.load_json_file(path)
|
data = ut.load_json_file(path)
|
||||||
if data != None:
|
if data != None:
|
||||||
self.fm.setup_file_rules(data, True)
|
self.fm.setup_file_rules(data, True)
|
||||||
log.info(f"Loaded custom file rules: {path}")
|
log.info(f"Loaded custom file rules: {path}")
|
||||||
|
|
||||||
def save_config(self, filepath=RULES_PATH):
|
def save_config(self, filepath=""):
|
||||||
|
"""Saves the file sorting rules to the given file path."""
|
||||||
|
if filepath == "": filepath = self.custom_rules_path
|
||||||
path = ut.parse_dir(filepath)
|
path = ut.parse_dir(filepath)
|
||||||
ut.save_json_file(path, {"filemodes": self.fm.filemodes})
|
ut.save_json_file(path, {"filemodes": self.fm.filemodes})
|
||||||
log.info(f"Saved custom file rules: {path}")
|
log.info(f"Saved custom file rules: {path}")
|
||||||
|
|
||||||
def reset_config(self):
|
def reset_config(self):
|
||||||
data = ut.load_json_file(self.fm.config_path)
|
"""Resets the file sorting rules back to default."""
|
||||||
|
data = ut.load_json_file(fm.Manager.RULES_PATH)
|
||||||
self.fm.setup_file_rules(data, True)
|
self.fm.setup_file_rules(data, True)
|
||||||
self.prep()
|
self.prep()
|
||||||
log.info(f"Reloaded default file rules")
|
log.info(f"Reloaded default file rules")
|
||||||
|
|
||||||
def update_mode_data(self, mode, key, value):
|
def update_mode_data(self, mode, key, value):
|
||||||
|
"""Updates file mode data (key and value)."""
|
||||||
|
if mode == "": mode = self.current_mode
|
||||||
if not mode in self.fm.filemodes: return
|
if not mode in self.fm.filemodes: return
|
||||||
self.fm.filemodes[mode].update({key: value})
|
self.fm.filemodes[mode].update({key: value})
|
||||||
log.info(f"Set properties for file mode: {key} = {value}")
|
log.info(f"Set properties for file mode: {key} = {value}")
|
||||||
|
|
||||||
def set_rules_variables(self, key):
|
def set_rules_variables(self, key):
|
||||||
|
"""Updates the internal variables of the file rules (key is mode)."""
|
||||||
if not key in self.fm.filemodes: return
|
if not key in self.fm.filemodes: return
|
||||||
data = self.fm.filemodes[key]
|
data = self.fm.filemodes[key]
|
||||||
self.v_radio.set(data["action"])
|
self.v_radio.set(data["action"])
|
||||||
self.v_targetdir.set(data["destination"])
|
self.v_targetdir.set(data["destination"])
|
||||||
|
# TODO: Change string parsing
|
||||||
if type(data["extensions"]) == list: # Needs improvement
|
if type(data["extensions"]) == list: # Needs improvement
|
||||||
self.v_extstr.set(", ".join(data["extensions"]))
|
self.v_extstr.set(", ".join(data["extensions"]))
|
||||||
else:
|
else:
|
||||||
self.v_extstr.set(data["extensions"])
|
self.v_extstr.set(data["extensions"])
|
||||||
|
|
||||||
def select_mode_from_list(self, event):
|
def select_mode_from_list(self, event):
|
||||||
|
"""Selects the current file mode to use; also updates variables."""
|
||||||
key = ""
|
key = ""
|
||||||
sel = self.w_list.curselection()
|
sel = self.w_list.curselection()
|
||||||
if len(sel) == 0: return
|
if len(sel) == 0: return
|
||||||
|
|
@ -134,26 +177,59 @@ class App(tk.Tk):
|
||||||
key = k
|
key = k
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(key) > 0:
|
if len(key) > 0: # Valid key was found
|
||||||
self.current_mode = key
|
self.current_mode = key
|
||||||
self.set_rules_variables(key)
|
self.set_rules_variables(key)
|
||||||
log.debug(f"Selected file mode: {key}")
|
log.debug(f"Selected file mode: {key}")
|
||||||
|
|
||||||
|
def ask_sort_directory(self):
|
||||||
|
"""Prompts which file directory to process files for the given mode."""
|
||||||
|
if not self.current_mode in self.fm.filemodes: return
|
||||||
|
data = self.fm.filemodes[self.current_mode]
|
||||||
|
dir = fd.askdirectory(initialdir = ut.parse_dir(data["destination"]),
|
||||||
|
title = "Select target directory")
|
||||||
|
self.v_targetdir.set(dir)
|
||||||
|
self.update_mode_data("", "destination", dir)
|
||||||
|
|
||||||
def set_directory(self, dir):
|
def set_directory(self, dir):
|
||||||
|
"""Sets the file directory for the app to use."""
|
||||||
self.fm.set_directory(dir)
|
self.fm.set_directory(dir)
|
||||||
|
log.debug(f"Set custom file directory: {dir}")
|
||||||
self.update_fileview()
|
self.update_fileview()
|
||||||
|
|
||||||
def ask_directory(self):
|
def ask_directory(self):
|
||||||
|
"""Prompts which file directory for the app to use."""
|
||||||
dir = fd.askdirectory(initialdir = ut.parse_dir(self.fm.dir),
|
dir = fd.askdirectory(initialdir = ut.parse_dir(self.fm.dir),
|
||||||
title = "Select directory")
|
title = "Select directory")
|
||||||
self.set_directory(dir)
|
self.set_directory(dir)
|
||||||
|
|
||||||
|
def ask_load_rules(self):
|
||||||
|
"""Prompts which file rules config for the app to use."""
|
||||||
|
initdir = ut.parse_dir(self.fm.config_path)
|
||||||
|
filetypes = (("Config files", "*.json"), ("All files", "*.*"))
|
||||||
|
dir = fd.askopenfilename(initialdir = initdir,
|
||||||
|
filetypes = filetypes,
|
||||||
|
title = "Load config file")
|
||||||
|
self.load_config(dir)
|
||||||
|
|
||||||
|
def ask_save_rules(self):
|
||||||
|
"""Prompts which file name to use to save the file rules config."""
|
||||||
|
initdir = ut.parse_dir(self.custom_rules_path)
|
||||||
|
filetypes = (("Config files", "*.json"))
|
||||||
|
dir = fd.asksaveasfilename(initialdir = initdir,
|
||||||
|
filetypes = filetypes,
|
||||||
|
title = "Save config file")
|
||||||
|
self.custom_rules_path = dir
|
||||||
|
self.save_config(dir)
|
||||||
|
|
||||||
def prep(self):
|
def prep(self):
|
||||||
|
"""Makes the app ready to use after initialization."""
|
||||||
self.update_fileview()
|
self.update_fileview()
|
||||||
if self.current_mode in self.fm.filemodes:
|
if self.current_mode in self.fm.filemodes:
|
||||||
self.set_rules_variables(self.current_mode)
|
self.set_rules_variables(self.current_mode)
|
||||||
|
|
||||||
def run_task(self):
|
def run_task(self):
|
||||||
|
"""Runs the main file sorting task."""
|
||||||
result = mb.askyesno("Task", "Ready to sort all files?")
|
result = mb.askyesno("Task", "Ready to sort all files?")
|
||||||
if not result: return
|
if not result: return
|
||||||
|
|
||||||
|
|
@ -168,6 +244,7 @@ class App(tk.Tk):
|
||||||
+ f"Deleted {stats['delete']} files.\n")
|
+ f"Deleted {stats['delete']} files.\n")
|
||||||
|
|
||||||
def gui(self):
|
def gui(self):
|
||||||
|
"""Main user interface of the app."""
|
||||||
self.gui_menu()
|
self.gui_menu()
|
||||||
|
|
||||||
self.book = ttk.Notebook(self)
|
self.book = ttk.Notebook(self)
|
||||||
|
|
@ -177,12 +254,25 @@ class App(tk.Tk):
|
||||||
self.book.pack(expand=True, fill=tk.BOTH)
|
self.book.pack(expand=True, fill=tk.BOTH)
|
||||||
|
|
||||||
def gui_menu(self):
|
def gui_menu(self):
|
||||||
a_menu = tk.Menu(self) # First arg is always the parent
|
"""File bar of the app."""
|
||||||
|
a_menu = tk.Menu(self)
|
||||||
self.config(menu=a_menu)
|
self.config(menu=a_menu)
|
||||||
|
|
||||||
a_menu_file = tk.Menu(a_menu)
|
a_menu_file = tk.Menu(a_menu)
|
||||||
a_menu_file.add_command(label="Set Directory",
|
a_menu_file.add_command(label="Open Directory",
|
||||||
command=self.ask_directory)
|
command=self.ask_directory)
|
||||||
|
a_menu_file.add_separator()
|
||||||
|
|
||||||
|
a_menu_file.add_command(label="Load Rules",
|
||||||
|
command=self.ask_load_rules)
|
||||||
|
a_menu_file.add_command(label="Save Rules",
|
||||||
|
command=self.save_config)
|
||||||
|
a_menu_file.add_command(label="Save Rules As...",
|
||||||
|
command=self.ask_save_rules)
|
||||||
|
a_menu_file.add_command(label="Reset Rules",
|
||||||
|
command=self.reset_config)
|
||||||
|
a_menu_file.add_separator()
|
||||||
|
|
||||||
a_menu_file.add_command(label="Exit", command=self.destroy)
|
a_menu_file.add_command(label="Exit", command=self.destroy)
|
||||||
|
|
||||||
a_menu.add_cascade(
|
a_menu.add_cascade(
|
||||||
|
|
@ -191,7 +281,22 @@ class App(tk.Tk):
|
||||||
underline=0
|
underline=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
a_menu_settings = tk.Menu(a_menu)
|
||||||
|
|
||||||
|
self.v_tree = tk.BooleanVar(value=True)
|
||||||
|
a_menu_settings.add_checkbutton(label="Show Categories",
|
||||||
|
variable=self.v_tree,
|
||||||
|
command=self.update_fileview)
|
||||||
|
|
||||||
|
a_menu.add_cascade(
|
||||||
|
label="Settings",
|
||||||
|
menu=a_menu_settings,
|
||||||
|
underline=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def gui_files(self):
|
def gui_files(self):
|
||||||
|
"""Files tab of the app."""
|
||||||
frame = ttk.Frame(self.book)
|
frame = ttk.Frame(self.book)
|
||||||
|
|
||||||
box = ttk.Frame(frame)
|
box = ttk.Frame(frame)
|
||||||
|
|
@ -202,23 +307,24 @@ class App(tk.Tk):
|
||||||
button = ttk.Button(box, text="Refresh", command=self.update_fileview)
|
button = ttk.Button(box, text="Refresh", command=self.update_fileview)
|
||||||
button.grid(column=1, row=0, pady=5)
|
button.grid(column=1, row=0, pady=5)
|
||||||
|
|
||||||
self.v_tree = tk.BooleanVar(value=False)
|
|
||||||
f_check = lambda: self.update_fileview()
|
|
||||||
check = ttk.Checkbutton(box, text="Show As Tree", variable=self.v_tree,
|
|
||||||
command=f_check)
|
|
||||||
check.grid(column=2, row=0, padx=5, pady=5)
|
|
||||||
|
|
||||||
box.pack(side=tk.TOP, fill=tk.X)
|
box.pack(side=tk.TOP, fill=tk.X)
|
||||||
|
|
||||||
self.fileview_columns = ("Last Modified", "Size")
|
self.fileview_columns = ("Last Modified", "Size")
|
||||||
self.fileview = ttk.Treeview(frame, columns=self.fileview_columns)
|
self.fileview = ttk.Treeview(frame, columns=self.fileview_columns)
|
||||||
# Disabled "show=headings"
|
# Disabled "show=headings"
|
||||||
|
|
||||||
self.fileview.heading("#0", text="Name") # First (key) column
|
temp_columns = (("name", "Name",
|
||||||
self.fileview.column("#0", width=200, anchor=tk.W)
|
lambda: self.sort_fileview_items("name")),
|
||||||
for v in self.fileview_columns: # Value columns
|
("time", "Last Modified",
|
||||||
self.fileview.heading(v, text=v)
|
lambda: self.sort_fileview_items("time")),
|
||||||
self.fileview.column(v, width=100, anchor=tk.W)
|
("size", "Size",
|
||||||
|
lambda: self.sort_fileview_items("size")))
|
||||||
|
|
||||||
|
for i, v in enumerate(temp_columns):
|
||||||
|
id = "#0" if i == 0 else v[1]
|
||||||
|
width = 200 if i == 0 else 100
|
||||||
|
self.fileview.heading(id, text=v[1], command=v[2])
|
||||||
|
self.fileview.column(id, width=width, anchor=tk.W)
|
||||||
|
|
||||||
sbar = ttk.Scrollbar(frame, orient=tk.VERTICAL,
|
sbar = ttk.Scrollbar(frame, orient=tk.VERTICAL,
|
||||||
command=self.fileview.yview)
|
command=self.fileview.yview)
|
||||||
|
|
@ -230,19 +336,10 @@ class App(tk.Tk):
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def gui_rules(self):
|
def gui_rules(self):
|
||||||
|
"""Rules tab of the app."""
|
||||||
um = self.update_mode_data
|
um = self.update_mode_data
|
||||||
frame = ttk.Frame(self.book)
|
frame = ttk.Frame(self.book)
|
||||||
|
|
||||||
box = ttk.Frame(frame)
|
|
||||||
|
|
||||||
button = ttk.Button(box, text="Save Config", command=self.save_config)
|
|
||||||
button.grid(column=0, row=0, pady=5)
|
|
||||||
|
|
||||||
button = ttk.Button(box, text="Reset", command=self.reset_config)
|
|
||||||
button.grid(column=1, row=0, pady=5)
|
|
||||||
|
|
||||||
box.pack(side=tk.TOP, fill=tk.X)
|
|
||||||
|
|
||||||
main = ttk.Frame(frame)
|
main = ttk.Frame(frame)
|
||||||
main.columnconfigure(0, weight=1, uniform="column")
|
main.columnconfigure(0, weight=1, uniform="column")
|
||||||
main.columnconfigure(1, weight=3, uniform="column")
|
main.columnconfigure(1, weight=3, uniform="column")
|
||||||
|
|
@ -260,7 +357,7 @@ class App(tk.Tk):
|
||||||
|
|
||||||
self.v_radio = tk.StringVar()
|
self.v_radio = tk.StringVar()
|
||||||
self.v_actions = ("move", "copy", "delete", "ignore")
|
self.v_actions = ("move", "copy", "delete", "ignore")
|
||||||
f_radio = lambda: um(self.current_mode, "action", self.v_radio.get())
|
f_radio = lambda: um("", "action", self.v_radio.get())
|
||||||
for s in self.v_actions:
|
for s in self.v_actions:
|
||||||
str = f"{s.capitalize()} "
|
str = f"{s.capitalize()} "
|
||||||
radio = ttk.Radiobutton(i_frame, text=str, value=s,
|
radio = ttk.Radiobutton(i_frame, text=str, value=s,
|
||||||
|
|
@ -270,17 +367,18 @@ class App(tk.Tk):
|
||||||
|
|
||||||
i_frame = ttk.Labelframe(details, text="Target Directory")
|
i_frame = ttk.Labelframe(details, text="Target Directory")
|
||||||
|
|
||||||
label = ttk.Label(i_frame,
|
# text="Start with \"~/\" for your home (user) directory.")
|
||||||
text="Start with \"~/\" "
|
|
||||||
"for your home (user) directory.")
|
|
||||||
label.pack(side=tk.TOP, anchor=tk.W)
|
|
||||||
|
|
||||||
self.v_targetdir = tk.StringVar()
|
self.v_targetdir = tk.StringVar()
|
||||||
f_targetdir = lambda e: um(self.current_mode, "destination", self.v_targetdir.get())
|
f_targetdir = lambda e: um("", "destination", self.v_targetdir.get())
|
||||||
entry = ttk.Entry(i_frame, textvariable=self.v_targetdir)
|
entry = ttk.Entry(i_frame, textvariable=self.v_targetdir)
|
||||||
entry.bind('<Return>', f_targetdir)
|
entry.bind('<Return>', f_targetdir)
|
||||||
entry.pack(side=tk.TOP, fill=tk.X)
|
entry.pack(side=tk.TOP, fill=tk.X)
|
||||||
|
|
||||||
|
f_userdir = lambda: self.ask_sort_directory()
|
||||||
|
button = ttk.Button(i_frame, text="Browse...", command=f_userdir)
|
||||||
|
button.pack(side=tk.TOP, anchor=tk.W)
|
||||||
|
|
||||||
i_frame.pack(side=tk.TOP, fill=tk.X, pady=5)
|
i_frame.pack(side=tk.TOP, fill=tk.X, pady=5)
|
||||||
|
|
||||||
i_frame = ttk.Labelframe(details, text="File Extensions")
|
i_frame = ttk.Labelframe(details, text="File Extensions")
|
||||||
|
|
@ -290,7 +388,7 @@ class App(tk.Tk):
|
||||||
label.pack(side=tk.TOP, anchor=tk.W)
|
label.pack(side=tk.TOP, anchor=tk.W)
|
||||||
|
|
||||||
self.v_extstr = tk.StringVar()
|
self.v_extstr = tk.StringVar()
|
||||||
f_extstr = lambda e: um(self.current_mode, "extensions", self.v_extstr.get())
|
f_extstr = lambda e: um("", "extensions", self.v_extstr.get())
|
||||||
entry = ttk.Entry(i_frame, textvariable=self.v_extstr)
|
entry = ttk.Entry(i_frame, textvariable=self.v_extstr)
|
||||||
entry.bind('<Return>', f_extstr)
|
entry.bind('<Return>', f_extstr)
|
||||||
entry.pack(side=tk.TOP, fill=tk.X)
|
entry.pack(side=tk.TOP, fill=tk.X)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"all": {
|
"all": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Downloads/",
|
"destination": "~/Downloads/FinchFiler/",
|
||||||
"extensions": ["*"],
|
"extensions": ["*"],
|
||||||
"name": "All Files"
|
"name": "All Files"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
"""File manager and organizer for the app."""
|
"""File manager and organizer module."""
|
||||||
|
|
||||||
__author__ = "Gull"
|
__author__ = "Gull"
|
||||||
|
|
||||||
|
|
@ -9,14 +9,17 @@ import utils as ut
|
||||||
import send2trash as s2t
|
import send2trash as s2t
|
||||||
|
|
||||||
debug_mode = True
|
debug_mode = True
|
||||||
SOURCE_PATH = "~/Downloads/"
|
|
||||||
RULES_PATH = "data/file_rules_default.json"
|
|
||||||
|
|
||||||
class Manager:
|
class Manager:
|
||||||
|
"""File manager and organizer system."""
|
||||||
|
# These constants are defined here, for cross module access
|
||||||
|
SOURCE_PATH = "~/Downloads/"
|
||||||
|
RULES_PATH = "data/file_rules_default.json"
|
||||||
|
|
||||||
"""File manager that can store and sort files in a directory."""
|
"""File manager that can store and sort files in a directory."""
|
||||||
def __init__(self, auto_config=False):
|
def __init__(self, auto_config=False):
|
||||||
self.dir = SOURCE_PATH # Default directory
|
self.dir = ut.parse_dir(Manager.SOURCE_PATH) # Current directory
|
||||||
self.config_path = RULES_PATH # Default file rules
|
self.config_path = Manager.RULES_PATH # Current file rules
|
||||||
self.filedata = dict() # Dictionary of files in directory
|
self.filedata = dict() # Dictionary of files in directory
|
||||||
self.filemodes = dict() # Contains file mode data
|
self.filemodes = dict() # Contains file mode data
|
||||||
self.filetypes = dict() # Maps file extensions to modes
|
self.filetypes = dict() # Maps file extensions to modes
|
||||||
|
|
@ -83,7 +86,7 @@ class Manager:
|
||||||
for entry in scan:
|
for entry in scan:
|
||||||
if entry.is_file():
|
if entry.is_file():
|
||||||
data = self.get_file_properties(entry.name)
|
data = self.get_file_properties(entry.name)
|
||||||
self.filedata[entry.name] = data
|
self.filedata.update({entry.name: data})
|
||||||
|
|
||||||
def run_task(self):
|
def run_task(self):
|
||||||
"""Sorts all files in the active directory, following file rules."""
|
"""Sorts all files in the active directory, following file rules."""
|
||||||
|
|
|
||||||
40
script.py
40
script.py
|
|
@ -6,34 +6,42 @@ import sys
|
||||||
import logging as log
|
import logging as log
|
||||||
import utils as ut
|
import utils as ut
|
||||||
import file_manager as fm
|
import file_manager as fm
|
||||||
|
from argparse import ArgumentParser as ap
|
||||||
|
|
||||||
LOG_PATH = "%/Gull/script.log"
|
LOG_PATH = "%/Gull/script.log"
|
||||||
SOURCE_PATH = "~/Downloads"
|
SOURCE_PATH = "~/Downloads"
|
||||||
RULES_PATH = "%/Gull/file_rules_custom.json"
|
RULES_PATH = "%/Gull/file_rules_custom.json"
|
||||||
MODES = {
|
|
||||||
"-a": "default",
|
|
||||||
"-m": "move",
|
|
||||||
"-c": "copy",
|
|
||||||
"-d": "delete",
|
|
||||||
"-i": "ignore"
|
|
||||||
}
|
|
||||||
|
|
||||||
def setup_log():
|
def setup_log():
|
||||||
log.basicConfig(**ut.get_log_config(LOG_PATH, log.DEBUG))
|
log.basicConfig(**ut.get_log_config(LOG_PATH, log.DEBUG))
|
||||||
log.getLogger().addHandler(log.StreamHandler(sys.stdout)) # Console
|
log.getLogger().addHandler(log.StreamHandler(sys.stdout)) # Console
|
||||||
log.info("Script started")
|
log.info("Script started")
|
||||||
|
|
||||||
def run(source=SOURCE_PATH, rules=RULES_PATH, mode="default", debug=None):
|
def run():
|
||||||
if source == "-d": source = SOURCE_PATH
|
parser = ap(prog="Finch Filer",
|
||||||
if rules == "-d": rules = RULES_PATH
|
description="Moves, copies, or deletes files from a source "
|
||||||
if mode in MODES: mode = MODES[mode]
|
"directory.",
|
||||||
if debug == "true" or debug == True: fm.set_debug_mode(True)
|
add_help=True)
|
||||||
|
parser.add_argument("-s", "--source", default=SOURCE_PATH,
|
||||||
|
help="source directory (default: User Downloads)")
|
||||||
|
parser.add_argument("-r", "--rules", default=RULES_PATH,
|
||||||
|
help="config file (default: Same as app setting)")
|
||||||
|
parser.add_argument("-m", "--mode", default="default",
|
||||||
|
help="action for all files (move, copy, delete, "
|
||||||
|
"ignore)")
|
||||||
|
parser.add_argument("-d", "--debug", action="store_true",
|
||||||
|
help="enables debug mode and ignores file actions")
|
||||||
|
|
||||||
source = ut.parse_dir(source, True)
|
args = parser.parse_args()
|
||||||
rules = ut.parse_dir(rules, True)
|
source = ut.parse_dir(args.source, True)
|
||||||
|
rules = ut.parse_dir(args.rules, True)
|
||||||
|
mode = args.mode
|
||||||
|
if args.debug: fm.set_debug_mode(True)
|
||||||
|
|
||||||
setup_log()
|
setup_log()
|
||||||
|
|
||||||
|
fm.set_debug_mode(True) # Temporary
|
||||||
|
|
||||||
mgr = fm.Manager()
|
mgr = fm.Manager()
|
||||||
mgr.set_directory(source)
|
mgr.set_directory(source)
|
||||||
mgr.setup_file_rules(ut.load_json_file(rules))
|
mgr.setup_file_rules(ut.load_json_file(rules))
|
||||||
|
|
@ -44,6 +52,4 @@ def run(source=SOURCE_PATH, rules=RULES_PATH, mode="default", debug=None):
|
||||||
mgr.update_file_data()
|
mgr.update_file_data()
|
||||||
mgr.run_task()
|
mgr.run_task()
|
||||||
|
|
||||||
args = sys.argv.copy()
|
run()
|
||||||
args.pop(0)
|
|
||||||
run(*args)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue