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 ("\n Found 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"\n Done. 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 ("\n Canceled by user." )
158+ if os .name == "nt" :
159+ os .system ("pause" )
0 commit comments