downsorter 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: downsorter
3
+ Version: 0.2.0
4
+ Summary: Organize files by extension into category folders from the command line.
5
+ Author: Your Name
6
+ License: MIT
7
+ Keywords: downloads,file sorting,organization,cli
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # Downsorter
19
+
20
+ A small Python CLI tool that organizes files by extension into category folders.
21
+
22
+ Use it to clean up a messy folder, especially downloads, by moving images, documents, archives, installers, code, audio, and video files into the right subfolders.
23
+
24
+ ## Features
25
+
26
+ - Organizes files by extension into category folders
27
+ - Preview mode so you can see changes before moving anything
28
+ - Safe move mode with `--apply` to actually perform the file moves
29
+ - Handles duplicate names by renaming files to `name (1).ext`, `name (2).ext`, etc.
30
+ - Ignores common system files like `desktop.ini` and `thumbs.db`
31
+ - Supports minimum file age filtering with `--min-age-days`
32
+ - Writes a CSV log of all moves for auditing
33
+ - Works on Windows, macOS, and Linux with Python 3.10+
34
+
35
+ ## Platform Support
36
+
37
+ | Platform | Support | Notes |
38
+ | --- | --- | --- |
39
+ | Windows 10/11 | Supported | Best tested platform for this project |
40
+ | macOS | Supported | Works with Python 3.10+ |
41
+ | Linux | Supported | Works with Python 3.10+ |
42
+
43
+ ## Files
44
+
45
+ - `sorter.py` - main CLI script and package entry point
46
+ - `README.md` - project instructions
47
+ - `pyproject.toml` - package metadata for PyPI
48
+ - `LICENSE` - MIT license text
49
+ - `movelogs.csv` - example move log file written by the script
50
+
51
+ ## Install
52
+
53
+ ### From source
54
+
55
+ ```bash
56
+ pip install .
57
+ ```
58
+
59
+ ### From PyPI
60
+
61
+ ```bash
62
+ pip install downsorter
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ ### Preview what will happen
68
+
69
+ This is the safest way to run the tool first.
70
+
71
+ ```bash
72
+ downsorter --folder "C:\Users\YourName\Downloads"
73
+ ```
74
+
75
+ ### Actually move files
76
+
77
+ ```bash
78
+ downsorter --folder "C:\Users\YourName\Downloads" --apply
79
+ ```
80
+
81
+ ### Only move files older than 7 days
82
+
83
+ ```bash
84
+ downsorter --folder "C:\Users\YourName\Downloads" --min-age-days 7 --apply
85
+ ```
86
+
87
+ ### Use a custom log file
88
+
89
+ ```bash
90
+ downsorter --folder "C:\Users\YourName\Downloads" --apply --log-file my-moves.csv
91
+ ```
92
+
93
+ ## Options
94
+
95
+ - `--folder`: Folder to organize. Defaults to the current user's Downloads folder.
96
+ - `--apply`: Actually move files. Without this flag, the script only previews the planned moves.
97
+ - `--min-age-days`: Only move files that have not been modified for at least this many days.
98
+ - `--log-file`: CSV file where completed moves are recorded.
99
+
100
+ ## How it works
101
+
102
+ 1. The script scans the target folder for files.
103
+ 2. It matches each file extension against a category list.
104
+ 3. It builds a plan showing where each file would move.
105
+ 4. In preview mode, it only prints the plan.
106
+ 5. In apply mode, it creates category folders, moves files, and logs the results.
107
+
108
+ ## Categories
109
+
110
+ The project currently sorts files into:
111
+
112
+ - `Images` (.jpg, .jpeg, .png, .webp, .bmp)
113
+ - `PDFs` (.pdf)
114
+ - `Documents` (.doc, .docx, .txt, .md, .rtf)
115
+ - `Spreadsheets` (.xls, .xlsx, .csv, .tsv)
116
+ - `PPTs` (.ppt, .pptx)
117
+ - `Archives` (.rar, .zip, .7z, .tar, .gz)
118
+ - `Installers` (.exe, .msi)
119
+ - `Code` (.py, .js, .html, .css, .json, .xml, .cpp, .java)
120
+ - `Audio` (.mp3, .wav, .flac, .aac, .ogg)
121
+ - `Videos` (.mp4, .gif, .mov, .mkv, .avi, .webm)
122
+ - `Other` for everything else
123
+
124
+ ## Example
125
+
126
+ Run this to preview then apply:
127
+
128
+ ```bash
129
+ downsorter --folder "C:\Users\YourName\Downloads"
130
+ downsorter --folder "C:\Users\YourName\Downloads" --apply
131
+ ```
132
+
133
+ ## License
134
+
135
+ This project is licensed under the MIT License.
@@ -0,0 +1,7 @@
1
+ sorter.py,sha256=qMkePEUX86d3jVeusBAI1G5WAhcF0vI6hNsxxnIxglQ,4880
2
+ downsorter-0.2.0.dist-info/licenses/LICENSE,sha256=S_fbX1OLfL7GYfxafKhn4E6-9cDMpbxy3-OgQisZr5A,1087
3
+ downsorter-0.2.0.dist-info/METADATA,sha256=SyTanw3fBs6CafOzB4YpkmTDxnvW6wYBOAiR0TVgRiA,3890
4
+ downsorter-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ downsorter-0.2.0.dist-info/entry_points.txt,sha256=_gGMAUB8BfuaeY3SBVlGPtjliLKhMgIFvZwxwkkESzk,43
6
+ downsorter-0.2.0.dist-info/top_level.txt,sha256=6PgYfJOJ6CnmzIwV3qA6Gso2mBYIELKxdlBfnm1CSzo,7
7
+ downsorter-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ downsorter = sorter:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ sorter
sorter.py ADDED
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ import argparse
5
+ import shutil
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timedelta
8
+ from pathlib import Path
9
+
10
+
11
+ CATEGORIES = {
12
+ "Images": {".jpg" , ".jpeg" , ".png" , ".webp" , ".bmp"},
13
+ "PDFs" : {".pdf"},
14
+ "Documents" : {".doc" , ".docx" , ".txt" , ".md" , ".rtf"},
15
+ "Spreadsheets" : {".xls" , ".xlsx" , ".csv" , ".tsv"},
16
+ "PPTs" : {".ppt" , ".pptx"},
17
+ "Archives" : {".rar" , ".zip" ,".7z" , ".tar" , ".gz"},
18
+ "Installers" : {".exe" , ".msi"},
19
+ "Code" : {".py" , ".js" , ".html" , ".css" , ".json" , ".xml" , ".cpp" , ".java"},
20
+ "Audio" : {".mp3" , ".wav" , ".flac" , ".aac" , ".ogg"},
21
+ "Videos" : {".mp4" , ".gif" , ".mov" , ".mkv" , ".avi" , ".webm"}
22
+ }
23
+ IGNORE_NAMES = {
24
+ "desktop.ini",
25
+ "thumbs.db",
26
+ }
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class MovePlan:
31
+ source: Path
32
+ destination: Path
33
+
34
+ def default_downloads_folder():
35
+ return Path.home()/ "Downloads"
36
+
37
+
38
+ def category_in(file_path: Path):
39
+ suffix = file_path.suffix.lower()
40
+ for category, extensions in CATEGORIES.items():
41
+ if suffix in extensions:
42
+ return category
43
+ return "Other"
44
+
45
+ def agecheck(file_path: Path, minimum_days: int):
46
+ if minimum_days <= 0:
47
+ return True
48
+ mod_time = datetime.fromtimestamp(file_path.stat().st_mtime)
49
+ return mod_time <= datetime.now() - timedelta(days= minimum_days)
50
+
51
+ def unique_des(des: Path):
52
+ if not des.exists():
53
+ return des
54
+ counter = 1
55
+ while True:
56
+ candidate = des.with_name(
57
+ f"{des.stem} ({counter}){des.suffix}"
58
+ )
59
+ if not candidate.exists():
60
+ return candidate
61
+ counter = counter + 1
62
+
63
+
64
+ def normalize_category_folder(downloads_folder: Path, category: str) -> Path:
65
+ canonical = downloads_folder / category
66
+ if canonical.exists():
67
+ return canonical
68
+
69
+ for item in downloads_folder.iterdir():
70
+ if item.is_dir() and item.name.lower() == category.lower():
71
+ temp = downloads_folder / f".{category}.tmp"
72
+ item.rename(temp)
73
+ temp.rename(canonical)
74
+ return canonical
75
+
76
+ return canonical
77
+
78
+
79
+ def BUILD_plan(downloads_folder:Path , minimum_days:int):
80
+ plans: list[MovePlan] = []
81
+ for item in downloads_folder.iterdir():
82
+ if not item.is_file():
83
+ continue
84
+ if item.name.lower() in IGNORE_NAMES:
85
+ continue
86
+ if not agecheck(item,minimum_days):
87
+ continue
88
+
89
+ category = category_in(item)
90
+ destination_folder = normalize_category_folder(downloads_folder, category)
91
+ des = unique_des(destination_folder / item.name)
92
+ plans.append(MovePlan(source=item , destination=des))
93
+ return plans
94
+
95
+ def PRINT_plan(plans :list[MovePlan] , apply : bool):
96
+ if not plans:
97
+ print("Nothing to clean , your folder is already tidy lmao")
98
+ return
99
+ action = "Moving" if apply else "Would move"
100
+ for plan in plans:
101
+ file_name = plan.source.name
102
+ destination = plan.destination.parent.name + "\\" + plan.destination.name
103
+ print(action + ": " + file_name + " -> " + destination)
104
+
105
+ def APPLY_plan(plans : list[MovePlan]):
106
+ for plan in plans:
107
+ plan.destination.parent.mkdir(parents=True , exist_ok=True)
108
+ shutil.move(str(plan.source) , str(plan.destination))
109
+
110
+ def parse_args():
111
+ parser = argparse.ArgumentParser(
112
+ description="Safely organize your downloads folder by file type"
113
+ )
114
+ parser.add_argument(
115
+ "--folder",
116
+ type=Path,
117
+ default=default_downloads_folder(),
118
+ help="Folder to clean . By default downloads folder"
119
+ )
120
+ parser.add_argument(
121
+ "--apply",
122
+ action="store_true",
123
+ help="Actually move files. Without this flag, the script only previews changes.",
124
+ )
125
+ parser.add_argument(
126
+ "--min-age-days",
127
+ type=int,
128
+ default=0,
129
+ help="Only move files at least this many days old. can be edited",
130
+ )
131
+ return parser.parse_args()
132
+
133
+ def main():
134
+ args = parse_args()
135
+ downloads_folder = args.folder.expanduser().resolve()
136
+
137
+ if not downloads_folder.exists():
138
+ raise SystemExit(f"Folder does not exist: {downloads_folder}")
139
+ if not downloads_folder.is_dir():
140
+ raise SystemExit(f"Not a folder: {downloads_folder}")
141
+ if args.min_age_days < 0:
142
+ raise SystemExit(f"--min-age-days cannot be NEGATIVE")
143
+
144
+ plans = BUILD_plan(downloads_folder, args.min_age_days)
145
+ PRINT_plan(plans , args.apply)
146
+
147
+ if args.apply and plans:
148
+ APPLY_plan(plans)
149
+ print("Done.")
150
+ elif plans:
151
+ print("Preview only. Run again with --apply to move these files")
152
+
153
+ if __name__=="__main__":
154
+ main()