Built the executable; fixed a few errors; updated the readme
This commit is contained in:
parent
94755aa78f
commit
d4d5096f1f
9 changed files with 65 additions and 18 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,4 +1,7 @@
|
||||||
.vscode/
|
.vscode/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
venv/
|
venv/
|
||||||
venv_linux/
|
venv_linux/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
Finch Filer.spec
|
||||||
|
|
|
||||||
BIN
Finch Filer.exe
Normal file
BIN
Finch Filer.exe
Normal file
Binary file not shown.
24
README.md
24
README.md
|
|
@ -1,10 +1,10 @@
|
||||||
# Finch Filer
|
# Finch Filer V1.0.0 beta
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
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).
|
A simple and humble 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:
|
Other use cases:
|
||||||
|
|
||||||
|
|
@ -12,12 +12,10 @@ Other use cases:
|
||||||
|
|
||||||
- Copying only certain types of documents into a backup folder
|
- Copying only certain types of documents into a backup folder
|
||||||
|
|
||||||
- Moving only files that contain the name "document"
|
- Moving only files that contain the name "invoice"
|
||||||
|
|
||||||
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.
|
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
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Manage and sort files in your Downloads directory, or any directory of your choice
|
- Manage and sort files in your Downloads directory, or any directory of your choice
|
||||||
|
|
@ -28,7 +26,21 @@ This is my first real Python project, after many unimportant scripts and experim
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can run either the app or the script
|
You can easily launch the app on Windows by entering into the "Finch Filer.exe" executable.
|
||||||
|
|
||||||
|
By default, the first thing you'll see is a list of all files in your Downloads directory. You can switch to a directory of your choice by going to File -> Open Directory. Currently, this app displays only items in the chosen directory and not nested ones. You can inspect this list, as well as sort by date and size by clicking on the column headers.
|
||||||
|
|
||||||
|
Importantly, you'll want to set up file rules if you want things to happen. Select the Rules tab. You'll see a list of selectable generic file types, and options for each one. This is how you can filter out which file types to keep and which ones to delete, for example. Note that the type All Files overrides all other types, unless its action is set to "Ignore".
|
||||||
|
|
||||||
|
For moving and copying all files of a given type, you can set the action as desired, and then set your destination directory. Also, you can customize the file names or extensions for the sorter to process. Each item should be separated by a comma.
|
||||||
|
|
||||||
|
When you are ready, go back to the Files tab and click on the Start Task button. The sorter will do its job, and a summary will be shown at the end.
|
||||||
|
|
||||||
|
For advanced usage, you can use the command line mode by opening your terminal at this directory and entering:
|
||||||
|
|
||||||
|
`python script.py`
|
||||||
|
|
||||||
|
This guide is incomplete. More info will be added later.
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
|
|
|
||||||
40
app.py
40
app.py
|
|
@ -5,7 +5,7 @@ File actions (move, copy, or delete) can be set for generic file types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__name__ = "__main__"
|
__name__ = "__main__"
|
||||||
__version__ = "1.0"
|
__version__ = "1.0.0"
|
||||||
__author__ = "Gull"
|
__author__ = "Gull"
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
|
@ -20,20 +20,34 @@ APP_NAME = "Finch Filer"
|
||||||
LOG_PATH = "%/Gullbase/FinchFiler/app.log"
|
LOG_PATH = "%/Gullbase/FinchFiler/app.log"
|
||||||
RULES_PATH = "%/Gullbase/FinchFiler/file_rules_custom.json"
|
RULES_PATH = "%/Gullbase/FinchFiler/file_rules_custom.json"
|
||||||
|
|
||||||
def setup_log():
|
"""
|
||||||
"""Initializes the logging system."""
|
TODO:
|
||||||
|
- Help dropdown on menu bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
"""Initializes the logging system, and a few other requirements."""
|
||||||
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))
|
||||||
log.getLogger().addHandler(log.StreamHandler(stdout)) # Console
|
log.getLogger().addHandler(log.StreamHandler(stdout)) # Console
|
||||||
log.info("App started")
|
log.info("App started")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ctypes import windll
|
||||||
|
|
||||||
|
myappid = "com.gullbase.finchfiler"
|
||||||
|
windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
class App(tk.Tk):
|
class App(tk.Tk):
|
||||||
"""TKinter GUI and related methods."""
|
"""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.option_add('*tearOff', tk.FALSE)
|
self.option_add('*tearOff', tk.FALSE)
|
||||||
|
self.iconbitmap(ut.parse_dir("#/icon.ico"))
|
||||||
self.fm = fm.Manager(True) # Main file manager object
|
self.fm = fm.Manager(True) # Main file manager object
|
||||||
self.log = list() # Running list of all logs
|
self.log = list() # Running list of all logs
|
||||||
self.temp = {"column": "", "reverse": False} # Temporary data
|
self.temp = {"column": "", "reverse": False} # Temporary data
|
||||||
|
|
@ -53,13 +67,13 @@ class App(tk.Tk):
|
||||||
def get_filemode_files(self, key, ignore_all=True):
|
def get_filemode_files(self, key, ignore_all=True):
|
||||||
"""Returns a dict of files that match the given file mode key."""
|
"""Returns a dict of files that match the given file mode key."""
|
||||||
if key == "all": # Special key representing all file types
|
if key == "all": # Special key representing all file types
|
||||||
return self.fm.filedata if not ignore_all else None
|
return self.fm.filedata if not ignore_all else dict()
|
||||||
|
|
||||||
files = dict()
|
files = dict()
|
||||||
for k, v in self.fm.filedata.items():
|
for k, v in self.fm.filedata.items():
|
||||||
if v["mode"] == key: files[k] = v
|
if v["mode"] == key: files[k] = v
|
||||||
|
|
||||||
return files if len(files) > 0 else None
|
return files
|
||||||
|
|
||||||
def update_fileview(self):
|
def update_fileview(self):
|
||||||
"""Updates the treeview widget in the app."""
|
"""Updates the treeview widget in the app."""
|
||||||
|
|
@ -75,7 +89,7 @@ class App(tk.Tk):
|
||||||
if not tree: filemodes = {"all": {"name": "All Files"}} # Fallback
|
if not tree: filemodes = {"all": {"name": "All Files"}} # Fallback
|
||||||
for k, v in filemodes.items():
|
for k, v in filemodes.items():
|
||||||
files = self.get_filemode_files(k, tree)
|
files = self.get_filemode_files(k, tree)
|
||||||
if files != None:
|
if len(files) > 0:
|
||||||
id = self.fileview.insert("", tk.END, text=v["name"],
|
id = self.fileview.insert("", tk.END, text=v["name"],
|
||||||
open=True)
|
open=True)
|
||||||
|
|
||||||
|
|
@ -124,6 +138,7 @@ class App(tk.Tk):
|
||||||
try:
|
try:
|
||||||
path = ut.parse_dir(filepath)
|
path = ut.parse_dir(filepath)
|
||||||
except FileNotFoundError as error:
|
except FileNotFoundError as error:
|
||||||
|
log.error("Failed to get file path for loading config file!")
|
||||||
print(error)
|
print(error)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
|
@ -132,6 +147,7 @@ class App(tk.Tk):
|
||||||
self.prep()
|
self.prep()
|
||||||
log.info(f"Loaded custom file rules: {path}")
|
log.info(f"Loaded custom file rules: {path}")
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
log.error("Failed to load config file!")
|
||||||
print(error)
|
print(error)
|
||||||
|
|
||||||
def save_config(self, filepath=""):
|
def save_config(self, filepath=""):
|
||||||
|
|
@ -140,12 +156,14 @@ class App(tk.Tk):
|
||||||
try:
|
try:
|
||||||
path = ut.parse_dir(filepath)
|
path = ut.parse_dir(filepath)
|
||||||
except FileNotFoundError as error:
|
except FileNotFoundError as error:
|
||||||
|
log.error("Failed to get file path for saving config file!")
|
||||||
print(error)
|
print(error)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
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}")
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
log.error("Failed to save config file!")
|
||||||
print(error)
|
print(error)
|
||||||
|
|
||||||
def reset_config(self):
|
def reset_config(self):
|
||||||
|
|
@ -155,8 +173,7 @@ class App(tk.Tk):
|
||||||
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")
|
||||||
except Exception as error:
|
except Exception:
|
||||||
print(error)
|
|
||||||
log.error("Failed to load default file rules!")
|
log.error("Failed to load default file rules!")
|
||||||
|
|
||||||
def update_mode_data(self, mode, key, value):
|
def update_mode_data(self, mode, key, value):
|
||||||
|
|
@ -269,7 +286,7 @@ class App(tk.Tk):
|
||||||
"will be affected.")
|
"will be affected.")
|
||||||
if not result: return
|
if not result: return
|
||||||
|
|
||||||
log.info("File organization started")
|
log.info("File manager task started")
|
||||||
self.fm.run_task()
|
self.fm.run_task()
|
||||||
|
|
||||||
stats = self.fm.work["stats"]
|
stats = self.fm.work["stats"]
|
||||||
|
|
@ -278,6 +295,9 @@ class App(tk.Tk):
|
||||||
+ f"Moved {stats['move']} files.\n"
|
+ f"Moved {stats['move']} files.\n"
|
||||||
+ f"Copied {stats['copy']} files.\n"
|
+ f"Copied {stats['copy']} files.\n"
|
||||||
+ f"Deleted {stats['delete']} files.\n")
|
+ f"Deleted {stats['delete']} files.\n")
|
||||||
|
|
||||||
|
log.info("File manager task finished")
|
||||||
|
self.update_fileview()
|
||||||
|
|
||||||
def gui(self):
|
def gui(self):
|
||||||
"""Main user interface of the app."""
|
"""Main user interface of the app."""
|
||||||
|
|
@ -434,6 +454,6 @@ class App(tk.Tk):
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
setup_log()
|
setup()
|
||||||
app = App()
|
app = App()
|
||||||
app.mainloop()
|
app.mainloop()
|
||||||
|
|
@ -162,6 +162,7 @@ def move(src, dst):
|
||||||
try:
|
try:
|
||||||
shutil.move(src, dst)
|
shutil.move(src, dst)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
log.error(f"Failed to move file: {src} -> {dst}")
|
||||||
print(error)
|
print(error)
|
||||||
return src, dst
|
return src, dst
|
||||||
|
|
||||||
|
|
@ -173,6 +174,7 @@ def copy(src, dst):
|
||||||
try:
|
try:
|
||||||
shutil.copy2(src, dst)
|
shutil.copy2(src, dst)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
log.error(f"Failed to copy file: {src} -> {dst}")
|
||||||
print(error)
|
print(error)
|
||||||
return src, dst
|
return src, dst
|
||||||
|
|
||||||
|
|
@ -184,5 +186,6 @@ def delete(src):
|
||||||
try:
|
try:
|
||||||
s2t.send2trash(src)
|
s2t.send2trash(src)
|
||||||
except s2t.TrashPermissionError as error:
|
except s2t.TrashPermissionError as error:
|
||||||
|
log.error(f"Failed to delete file: {src}")
|
||||||
print(error)
|
print(error)
|
||||||
return src
|
return src
|
||||||
BIN
icon.ico
Normal file
BIN
icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
__author__ = "Gull"
|
__author__ = "Gull"
|
||||||
|
|
||||||
import sys
|
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
|
||||||
|
|
@ -63,5 +63,6 @@ def run():
|
||||||
|
|
||||||
mgr.update_file_data()
|
mgr.update_file_data()
|
||||||
mgr.run_task()
|
mgr.run_task()
|
||||||
|
log.info("Script finished")
|
||||||
|
|
||||||
run()
|
run()
|
||||||
8
utils.py
8
utils.py
|
|
@ -3,6 +3,7 @@
|
||||||
__author__ = "Gull"
|
__author__ = "Gull"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
@ -58,6 +59,13 @@ def parse_dir(path="", ignore_mkdir=False):
|
||||||
path = pathlib.Path(path).expanduser()
|
path = pathlib.Path(path).expanduser()
|
||||||
elif path[0] == "%": # Denotes the local appdata directory
|
elif path[0] == "%": # Denotes the local appdata directory
|
||||||
path = pathlib.Path(ad.user_data_dir(None, False)) / path.lstrip("%/")
|
path = pathlib.Path(ad.user_data_dir(None, False)) / path.lstrip("%/")
|
||||||
|
elif path[0] == "#": # Denotes the project dev directory
|
||||||
|
try:
|
||||||
|
base_path = sys._MEIPASS # For PyInstaller
|
||||||
|
except Exception:
|
||||||
|
base_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
path = pathlib.Path(base_path) / path.lstrip("#/")
|
||||||
else:
|
else:
|
||||||
path = pathlib.Path(path)
|
path = pathlib.Path(path)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue