downsorter 0.1.0__tar.gz
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.
- downsorter-0.1.0/LICENSE +21 -0
- downsorter-0.1.0/PKG-INFO +47 -0
- downsorter-0.1.0/README.md +30 -0
- downsorter-0.1.0/downsorter.egg-info/PKG-INFO +47 -0
- downsorter-0.1.0/downsorter.egg-info/SOURCES.txt +9 -0
- downsorter-0.1.0/downsorter.egg-info/dependency_links.txt +1 -0
- downsorter-0.1.0/downsorter.egg-info/entry_points.txt +2 -0
- downsorter-0.1.0/downsorter.egg-info/top_level.txt +1 -0
- downsorter-0.1.0/pyproject.toml +28 -0
- downsorter-0.1.0/setup.cfg +4 -0
- downsorter-0.1.0/sorter.py +160 -0
downsorter-0.1.0/LICENSE
ADDED
|
@@ -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,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: downsorter
|
|
3
|
+
Version: 0.1.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 simple Python CLI tool to organize files in a folder by extension into category folders.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Preview mode:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
downsorter --folder "C:\path\to\folder"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Apply mode:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
downsorter --folder "C:\path\to\folder" --apply
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
- `--folder`: Folder to organize (defaults to your Downloads folder)
|
|
45
|
+
- `--apply`: Actually move files
|
|
46
|
+
- `--min-age-days`: Only move files at least this many days old
|
|
47
|
+
- `--log-file`: CSV file to write move logs
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Downsorter
|
|
2
|
+
|
|
3
|
+
A simple Python CLI tool to organize files in a folder by extension into category folders.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Preview mode:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
downsorter --folder "C:\path\to\folder"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Apply mode:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
downsorter --folder "C:\path\to\folder" --apply
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Options
|
|
26
|
+
|
|
27
|
+
- `--folder`: Folder to organize (defaults to your Downloads folder)
|
|
28
|
+
- `--apply`: Actually move files
|
|
29
|
+
- `--min-age-days`: Only move files at least this many days old
|
|
30
|
+
- `--log-file`: CSV file to write move logs
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: downsorter
|
|
3
|
+
Version: 0.1.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 simple Python CLI tool to organize files in a folder by extension into category folders.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Preview mode:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
downsorter --folder "C:\path\to\folder"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Apply mode:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
downsorter --folder "C:\path\to\folder" --apply
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
- `--folder`: Folder to organize (defaults to your Downloads folder)
|
|
45
|
+
- `--apply`: Actually move files
|
|
46
|
+
- `--min-age-days`: Only move files at least this many days old
|
|
47
|
+
- `--log-file`: CSV file to write move logs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sorter
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "downsorter"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Organize files by extension into category folders from the command line."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Your Name" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["downloads", "file sorting", "organization", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: Microsoft :: Windows",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Topic :: Utilities",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
downsorter = "sorter:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
py-modules = ["sorter"]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import csv
|
|
6
|
+
import shutil
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
CATEGORIES = {
|
|
13
|
+
"Images": {".jpg" , ".jpeg" , ".png" , ".webp" , ".bmp"},
|
|
14
|
+
"PDFs" : {".pdf"},
|
|
15
|
+
"Documents" : {".doc" , ".docx" , ".txt" , ".md" , ".rtf"},
|
|
16
|
+
"Spreadsheets" : {".xls" , ".xlsx" , ".csv" , ".tsv"},
|
|
17
|
+
"PPTs" : {".ppt" , ".pptx"},
|
|
18
|
+
"archives" : {".rar" , ".zip" ,".7z" , ".tar" , ".gz"},
|
|
19
|
+
"installers" : {".exe" , ".msi"},
|
|
20
|
+
"Code" : {".py" , ".js" , ".html" , ".css" , ".json" , ".xml" , ".cpp" , ".java"},
|
|
21
|
+
"Audio" : {".mp3" , ".wav" , ".flac" , ".aac" , ".ogg"},
|
|
22
|
+
"Videos" : {".mp4" , ".gif" , ".mov" , ".mkv" , ".avi" , ".webm"}
|
|
23
|
+
}
|
|
24
|
+
IGNORE_NAMES = {
|
|
25
|
+
"desktop.ini",
|
|
26
|
+
"thumbs.db",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class MovePlan:
|
|
32
|
+
source: Path
|
|
33
|
+
destination: Path
|
|
34
|
+
|
|
35
|
+
def default_downloads_folder():
|
|
36
|
+
return Path.home()/ "Downloads"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def category_in(file_path: Path):
|
|
40
|
+
suffix = file_path.suffix.lower()
|
|
41
|
+
for category, extensions in CATEGORIES.items():
|
|
42
|
+
if suffix in extensions:
|
|
43
|
+
return category
|
|
44
|
+
return "Other"
|
|
45
|
+
|
|
46
|
+
def agecheck(file_path: Path, minimum_days: int):
|
|
47
|
+
if minimum_days <= 0:
|
|
48
|
+
return True
|
|
49
|
+
mod_time = datetime.fromtimestamp(file_path.stat().st_mtime)
|
|
50
|
+
return mod_time <= datetime.now() - timedelta(days= minimum_days)
|
|
51
|
+
|
|
52
|
+
def unique_des(des: Path):
|
|
53
|
+
if not des.exists():
|
|
54
|
+
return des
|
|
55
|
+
counter = 1
|
|
56
|
+
while True:
|
|
57
|
+
candidate=des.with_name(
|
|
58
|
+
f"{des.stem} ({counter}){des.suffix}"
|
|
59
|
+
)
|
|
60
|
+
if not candidate.exists():
|
|
61
|
+
return candidate
|
|
62
|
+
counter = counter + 1
|
|
63
|
+
|
|
64
|
+
def BUILD_plan(downloads_folder:Path , minimum_days:int):
|
|
65
|
+
plans: list[MovePlan] = []
|
|
66
|
+
for item in downloads_folder.iterdir():
|
|
67
|
+
if not item.is_file():
|
|
68
|
+
continue
|
|
69
|
+
if item.name.lower() in IGNORE_NAMES:
|
|
70
|
+
continue
|
|
71
|
+
if not agecheck(item,minimum_days):
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
category = category_in(item)
|
|
75
|
+
des = unique_des(downloads_folder / category / item.name)
|
|
76
|
+
plans.append(MovePlan(source=item , destination=des))
|
|
77
|
+
return plans
|
|
78
|
+
|
|
79
|
+
def PRINT_plan(plans :list[MovePlan] , apply : bool):
|
|
80
|
+
if not plans:
|
|
81
|
+
print("Nothing to clean , your folder is already tidy lmao")
|
|
82
|
+
return
|
|
83
|
+
action = "Moving" if apply else "Would move"
|
|
84
|
+
for plan in plans:
|
|
85
|
+
file_name = plan.source.name
|
|
86
|
+
destination = plan.destination.parent.name + "\\" + plan.destination.name
|
|
87
|
+
print(action + ": " + file_name + " -> " + destination)
|
|
88
|
+
|
|
89
|
+
def WRITE_log(log_file: Path , completed_changes : list[MovePlan]):
|
|
90
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
file_exists=log_file.exists()
|
|
92
|
+
with log_file.open("a" , newline="",encoding="utf-8") as f:
|
|
93
|
+
writer = csv.writer(f)
|
|
94
|
+
if not file_exists:
|
|
95
|
+
writer.writerow(["moved at", "source", "destination"])
|
|
96
|
+
moved_at = datetime.now().isoformat(timespec="seconds")
|
|
97
|
+
for move in completed_changes:
|
|
98
|
+
writer.writerow([moved_at, str(move.source) , str(move.destination)])
|
|
99
|
+
|
|
100
|
+
def APPLY_plan(plans : list[MovePlan] , log_file: Path):
|
|
101
|
+
completed_changes : list[MovePlan] = []
|
|
102
|
+
|
|
103
|
+
for plan in plans:
|
|
104
|
+
plan.destination.parent.mkdir(parents=True , exist_ok=True)
|
|
105
|
+
shutil.move(str(plan.source) , str(plan.destination))
|
|
106
|
+
completed_changes.append(plan)
|
|
107
|
+
if completed_changes:
|
|
108
|
+
WRITE_log(log_file,completed_changes)
|
|
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
|
+
parser.add_argument(
|
|
132
|
+
"--log-file",
|
|
133
|
+
type=Path,
|
|
134
|
+
default=Path("movelogs.csv"),
|
|
135
|
+
help="CSV file where completed moves are recorded.",
|
|
136
|
+
)
|
|
137
|
+
return parser.parse_args()
|
|
138
|
+
|
|
139
|
+
def main():
|
|
140
|
+
args = parse_args()
|
|
141
|
+
downloads_folder = args.folder.expanduser().resolve()
|
|
142
|
+
|
|
143
|
+
if not downloads_folder.exists():
|
|
144
|
+
raise SystemExit(f"Folder does not exist: {downloads_folder}")
|
|
145
|
+
if not downloads_folder.is_dir():
|
|
146
|
+
raise SystemExit(f"Not a folder: {downloads_folder}")
|
|
147
|
+
if args.min_age_days < 0:
|
|
148
|
+
raise SystemExit(f"--min-age-days cannot be NEGATIVE")
|
|
149
|
+
|
|
150
|
+
plans = BUILD_plan(downloads_folder, args.min_age_days)
|
|
151
|
+
PRINT_plan(plans , args.apply)
|
|
152
|
+
|
|
153
|
+
if args.apply and plans:
|
|
154
|
+
APPLY_plan(plans , args.log_file)
|
|
155
|
+
print(f"Done. Move log written to {args.log_file.resolve()}")
|
|
156
|
+
elif plans:
|
|
157
|
+
print("Preview only. Run again with --apply to move these files")
|
|
158
|
+
|
|
159
|
+
if __name__=="__main__":
|
|
160
|
+
main()
|