Skip to content

Commit f995a02

Browse files
committed
Add the script code
1 parent 1928f5d commit f995a02

File tree

1 file changed

+159
-0
lines changed

1 file changed

+159
-0
lines changed

folder_sorter.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Folder Sorter: drop this script into any folder and run it.
4+
- Shows counts by file type (Audio, Images, Videos, Archives, Executables, Other)
5+
- Press Enter to proceed with sorting, or Esc to cancel.
6+
- Creates subfolders and moves files. Name conflicts are resolved safely.
7+
"""
8+
9+
from pathlib import Path
10+
import shutil
11+
import sys
12+
import os
13+
14+
# ---------- Configuration ----------
15+
CATEGORY_FOLDERS = {
16+
"Audio": {"mp3", "wav", "flac", "aac", "ogg", "m4a", "wma", "aiff", "alac", "opus"},
17+
"Images": {"jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp", "heic", "svg"},
18+
"Videos": {"mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v", "mpeg", "mpg"},
19+
"Executables": {"exe", "msi"},
20+
}
21+
22+
ARCHIVE_SUFFIX_PATTERNS = [
23+
".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst",
24+
]
25+
ARCHIVE_SINGLE_EXTS = {"zip", "rar", "7z", "gz", "bz2", "xz", "zst", "cab", "iso"}
26+
27+
DEST_NAMES = ["Audio", "Images", "Videos", "Archives", "Executables", "Other"]
28+
29+
# ---------- Keypress helpers ----------
30+
def wait_for_enter_or_esc() -> str:
31+
try:
32+
import msvcrt
33+
print("Press Enter to sort, or Esc to cancel...")
34+
while True:
35+
ch = msvcrt.getch()
36+
if ch in (b"\r", b"\n"):
37+
return "enter"
38+
if ch == b"\x1b":
39+
return "esc"
40+
except ImportError:
41+
import termios, tty
42+
fd = sys.stdin.fileno()
43+
old = termios.tcgetattr(fd)
44+
try:
45+
tty.setraw(fd)
46+
print("Press Enter to sort, or Esc to cancel...")
47+
while True:
48+
ch = sys.stdin.read(1)
49+
if ch == "\r" or ch == "\n":
50+
return "enter"
51+
if ch == "\x1b":
52+
return "esc"
53+
finally:
54+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
55+
56+
# ---------- Categorization ----------
57+
def categorize(file: Path) -> str:
58+
name_lower = file.name.lower()
59+
for pat in ARCHIVE_SUFFIX_PATTERNS:
60+
if name_lower.endswith(pat):
61+
return "Archives"
62+
ext = file.suffix.lower().lstrip(".")
63+
if ext in ARCHIVE_SINGLE_EXTS:
64+
return "Archives"
65+
if ext in CATEGORY_FOLDERS["Executables"]:
66+
return "Executables"
67+
for cat, exts in CATEGORY_FOLDERS.items():
68+
if cat == "Executables":
69+
continue
70+
if ext in exts:
71+
return cat
72+
return "Other"
73+
74+
# ---------- File moving with safe rename ----------
75+
def safe_move(src: Path, dst_dir: Path) -> Path:
76+
dst_dir.mkdir(parents=True, exist_ok=True)
77+
target = dst_dir / src.name
78+
if not target.exists():
79+
return src.rename(target)
80+
stem = target.stem
81+
suffix = target.suffix
82+
n = 1
83+
while True:
84+
candidate = dst_dir / f"{stem} ({n}){suffix}"
85+
if not candidate.exists():
86+
return src.rename(candidate)
87+
n += 1
88+
89+
# ---------- Main ----------
90+
def main():
91+
# --- Determine working folder ---
92+
if getattr(sys, 'frozen', False):
93+
# Running as a bundled EXE (PyInstaller)
94+
script_path = Path(sys.executable).resolve()
95+
else:
96+
# Running from .py
97+
script_path = Path(__file__).resolve()
98+
99+
root = script_path.parent
100+
101+
dest_dirs = {name: root / name for name in DEST_NAMES}
102+
files = [p for p in root.iterdir() if p.is_file() and p != script_path]
103+
104+
counts = {name: 0 for name in DEST_NAMES}
105+
candidates = []
106+
for f in files:
107+
if f.parent.name in DEST_NAMES:
108+
continue
109+
cat = categorize(f)
110+
counts[cat] += 1
111+
candidates.append((f, cat))
112+
113+
total = sum(counts.values())
114+
115+
print("\nFound the following files in:", root)
116+
for name in DEST_NAMES:
117+
print(f" {name:12} : {counts[name]}")
118+
print(f" {'Total':12} : {total}\n")
119+
120+
if total == 0:
121+
print("Nothing to do. No files to sort.")
122+
if os.name == "nt":
123+
os.system("pause")
124+
return
125+
126+
choice = wait_for_enter_or_esc()
127+
if choice == "esc":
128+
print("Canceled. No changes made.")
129+
if os.name == "nt":
130+
os.system("pause")
131+
return
132+
133+
moved = 0
134+
errors = 0
135+
for f, cat in candidates:
136+
try:
137+
target_dir = dest_dirs[cat]
138+
if f.parent == target_dir:
139+
continue
140+
safe_move(f, target_dir)
141+
moved += 1
142+
except Exception as e:
143+
print(f" ! Error moving '{f.name}': {e}")
144+
errors += 1
145+
146+
print(f"\nDone. Moved {moved} file(s).")
147+
if errors:
148+
print(f"Encountered {errors} error(s). See messages above.")
149+
150+
if os.name == "nt":
151+
os.system("pause")
152+
153+
if __name__ == "__main__":
154+
try:
155+
main()
156+
except KeyboardInterrupt:
157+
print("\nCanceled by user.")
158+
if os.name == "nt":
159+
os.system("pause")

0 commit comments

Comments
 (0)