Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions bulk-file-organizer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Virtual environment
venv/
.env/
.venv/

# Pytest cache
.pytest_cache/

# VS Code settings
.vscode/

# macOS metadata
.DS_Store

# Coverage reports
htmlcov/
.coverage
coverage.xml
*.cover

# Build artifacts
build/
dist/
*.egg-info/

# Logs and temp files
*.log
*.tmp
*.bak

# MyPy and Ruff
.mypy_cache/
.ruff_cache/

# Jupyter Notebook checkpoints (if any)
.ipynb_checkpoints/

# Test output
test-output/
test/
124 changes: 124 additions & 0 deletions bulk-file-organizer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# 🗂️ Bulk File Organizer — Integration Module

**Bulk File Organizer** is a Python command-line tool that automatically sorts files in a directory based on their extensions.
It transforms digital clutter into clean, structured folders — with vibrant terminal feedback powered by **Rich** ✨

---

## 📣 Latest Update: Integrated Module

This release brings integration with the [Bulk File Organizer](https://github.com/Shravan250/bulk-file-organizer) — a CLI utility by [Shravan Bobade](https://github.com/Shravan250) that organizes files by type, supports dry-run simulations, and provides colorful output using the `rich` library.

### ✨ Highlights

- 📦 Automatically sorts files into categorized folders (Images, Documents, Archives, etc.)
- 🔁 Recursive directory scanning
- 🧪 Dry-run simulation mode (no files moved)
- 🌈 Beautiful progress bars & summary tables
- ⚙️ Extensible via custom mappings

---

## ⚙️ Installation

```bash
pip install bulk-file-organizer
```

Or install directly from source:

```bash
git clone https://github.com/Shravan250/bulk-file-organizer.git
cd bulk-file-organizer
pip install .
```

---

## 🖥️ Usage

```bash
bulk-organizer /path/to/your/folder
```

### Options

| Flag | Description |
| ------------- | ------------------------------------------ |
| `--dry-run` | Simulate organization without moving files |
| `--summary` | Display a summary table after organization |
| `--recursive` | Recursively organize subdirectories |
| `--version` | Display the current version |

#### Example

```bash
bulk-organizer ~/Downloads --dry-run --summary
```

---

## 🧠 Example Output

```text
──────────────────────────── Bulk Organizer v0.1.0 ─────────────────────────────
Target Directory: ~/Downloads

Dry Run Mode Enabled — no files will be moved.

✨ Organizing files... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:02

📊 Organization Summary

┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Folder ┃ File Count ┃ Example Files ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Images │ 12 │ photo.png, logo.jpg, … │
│ Documents │ 8 │ resume.pdf, notes.txt, … │
│ Archives │ 2 │ backup.zip, logs.tar.gz │
└───────────┴────────────┴──────────────────────────────┘
──────────────────────────── Operation Complete ─────────────────────────────
```

---

## 🧩 Project Structure

```
bulk-file-organizer/
├── bulk_organizer/
│ ├── __init__.py
│ ├── cli.py
│ ├── scanner.py
│ ├── mapper.py
│ ├── organizer.py
│ └── utils.py
├── tests/
├── pyproject.toml
├── README.md
├── CONTRIBUTING.md
└── LICENSE
```

---

## 🤝 Contributing

We love clean code and clever minds.
Check out the [CONTRIBUTING.md](https://github.com/Shravan250/bulk-file-organizer/blob/main/CONTRIBUTING.md) to learn how you can contribute and make this project even better.

---

## 🪪 License

Licensed under the **MIT License**.

---

## ⭐ Show Some Love

If this project helped you bring order to your chaos —
please consider giving it a ⭐ on [GitHub](https://github.com/Shravan250/bulk-file-organizer)!
Every star helps this project grow and keeps the world just a little more organized 🌍✨
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Metadata-Version: 2.4
Name: bulk-file-organizer
Version: 0.1.0
Summary: A CLI tool to organize files into subfolders based on file extensions.
Author: Shravan
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: black; extra == "dev"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
README.md
pyproject.toml
bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO
bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt
bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt
bulk_organizer/bulk_file_organizer.egg-info/requires.txt
bulk_organizer/bulk_file_organizer.egg-info/top_level.txt
tests/test_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

[dev]
pytest
ruff
black
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

85 changes: 85 additions & 0 deletions bulk-file-organizer/bulk_organizer/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import argparse
import tomllib
from pathlib import Path
from rich.console import Console
from rich.table import Table
from rich.progress import track
from bulk_organizer.scanner import scanner
from bulk_organizer.mapper import map_extension_to_folder, DEFAULT_MAP
from bulk_organizer.organizer import organize_file

console = Console()

def get_version():
toml_path = Path(__file__).resolve().parent.parent / "pyproject.toml"
with open(toml_path, "rb") as f:
pyproject = tomllib.load(f)
return pyproject["project"]["version"]


def main():
parser = argparse.ArgumentParser()

organized = {}
CATEGORIZED_FOLDERS = set(DEFAULT_MAP.keys()) | {"Others"}

parser.add_argument("--version", action="version", version=f"bulk-organizer {get_version()}")
parser.add_argument("directory", type=str, help="Path to the directory to organize")
parser.add_argument("--dry-run", action="store_true", help="Simulate the organization without making changes")
parser.add_argument("--recursive", action="store_true", default=True, help="Recursively scan subdirectories")
parser.add_argument("--summary", action="store_true",default=True, help="Show compact summary instead of full paths")


args = parser.parse_args()
directory_path = Path(args.directory)
console.rule(f"[bold cyan]Bulk Organizer v{get_version()}[/bold cyan]")
console.print(f"Target Directory: [bold yellow]{directory_path}[/bold yellow]\n")


if args.dry_run:
console.print("[bold magenta]Dry Run Mode Enabled[/bold magenta] — no files will be moved.\n")

# Scan and print file paths
try:
files = list(scanner(directory_path, recursive=args.recursive))
first = True
for file_path in track(files, description="✨ Organizing files..."):
if first:
console.print()
console.print()
first = False

folder = map_extension_to_folder(file_path)

# Skip already-organized files
if file_path.parent.name in CATEGORIZED_FOLDERS:
continue

# Always track for summary
organized.setdefault(folder, []).append(file_path)

# Perform move or simulate
organize_file(file_path, directory_path, folder, dry_run=args.dry_run)

console.print()

if args.summary:
console.print("\n📊 [bold underline]Organization Summary[/bold underline]\n")
table = Table(show_header=True, header_style="bold blue")
table.add_column("Folder", style="cyan")
table.add_column("File Count", style="green")
table.add_column("Example Files", style="yellow")

for folder, files in organized.items():
examples = ", ".join(f.name for f in files[:3])
table.add_row(folder, str(len(files)), examples + (" ..." if len(files) > 3 else ""))

console.print(table)

console.rule("[bold green]Operation Complete[/bold green]")

except ValueError as e:
console.print(f"[bold red]Error:[/bold red] {e}")

if __name__ == "__main__":
main()
27 changes: 27 additions & 0 deletions bulk-file-organizer/bulk_organizer/mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pathlib import Path
from bulk_organizer.utils import normalize_extension


DEFAULT_MAP = {
"Images": [".jpg", ".jpeg", ".png", ".gif"],
"Documents": [".pdf", ".docx", ".txt"],
"Archives": [".zip", ".tar", ".gz"],
"Audio": [".mp3", ".wav"],
}

def map_extension_to_folder(file_path: Path, mapping: dict = DEFAULT_MAP) -> str:
"""Map a file extension to its corresponding folder name.

Args:
file_path (Path): The file path to map.
mapping (dict): A dictionary mapping folder names to lists of extensions.

Returns:
str: The folder name corresponding to the file's extension, or "Others" if not found.
"""
extension = normalize_extension(file_path.suffix)

for folder , extensions in mapping.items():
if extension in extensions:
return folder
return "Others"
38 changes: 38 additions & 0 deletions bulk-file-organizer/bulk_organizer/organizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import shutil
from pathlib import Path
from rich.console import Console

console = Console()

def organize_file(file_path: Path, base_dir: Path, folder_name: str, dry_run: bool = False):
"""Move a file into its categorized folder, handling collisions and dry-run mode.

Args:
file_path (Path): The original file path.
base_dir (Path): The base directory where folders are created.
folder_name (str): The target folder name.
dry_run (bool): If True, simulate the move without performing it.
"""
target_dir = base_dir / folder_name
target_dir.mkdir(parents=True, exist_ok=True)
target_path = target_dir / file_path.name

# handle name collisions
counter = 1
while target_path.exists():
target_path = target_dir / f"{file_path.stem} {counter}{file_path.suffix}"
counter += 1

if dry_run:
console.print(
f"[dim yellow][Dry Run][/dim yellow] {file_path.relative_to(base_dir)} → "
f"[bold cyan]{target_path.relative_to(base_dir)}[/bold cyan]"
)
else:
shutil.move(str(file_path), str(target_path))
console.print(
f"[green]✔ Moved[/green] [bold white]{file_path.relative_to(base_dir)}[/bold white] → "
f"[bold cyan]{target_path.relative_to(base_dir)}[/bold cyan]"
)


25 changes: 25 additions & 0 deletions bulk-file-organizer/bulk_organizer/scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path
from typing import Iterator

def scanner(directory: Path, recursive: bool = True) -> Iterator[Path]:
"""Scan the given directory and yield file paths.

Args:
directory (Path): The directory to scan.
recursive (bool): Whether to scan subdirectories recursively.

Yields:
Iterator[Path]: An iterator of file paths.
"""

if not directory.exists() or not directory.is_dir():
raise ValueError(f"{directory} is not a valid directory")

if recursive:
yield from (p for p in directory.rglob("*") if p.is_file())
else:
yield from (p for p in directory.iterdir() if p.is_file())




2 changes: 2 additions & 0 deletions bulk-file-organizer/bulk_organizer/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def normalize_extension(ext: str) -> str:
return ext.lower().strip()
Loading