#!/usr/bin/env python3
import os
import shutil
import unicodedata
import re
from pathlib import Path
import sys
# Copy a directory (~/Masic/RythmBox) to a FAT32 formated
# flash drive, named MUSIC, converting all filenames
# This was written with GPT-5 Mini via duck.ai as a bash script,
# Some manual changes and it was fed into Google Gemini
# Gemini then made further optimizations and rewrote it in python
# --- CONFIGURATION ---
HOME = Path.home()
SOURCE_DIR = HOME / "Music/RythmBox"
DEST_DIR = Path("/media") / HOME.name / "MUSIC"
MAX_BYTES = 255
# --- PROGRESS BAR SETUP ---
# Try to import tqdm. If missing, create a dummy fallback.
try:
from tqdm import tqdm
HAS_TQDM = True
except ImportError:
HAS_TQDM = False
print("Tip: Run 'pip3 install tqdm' for a nicer progress bar with ETA.")
# Fallback class to mimic tqdm if library is missing
class tqdm:
def __init__(self, total=0, unit='files', **kwargs):
self.total = total
self.n = 0
def update(self, n=1):
self.n += n
sys.stdout.write(f"\rProcessed: {self.n}/{self.total}")
sys.stdout.flush()
def close(self):
print("")
def write(self, msg):
sys.stdout.write(f"\r{msg}\n")
def __enter__(self): return self
def __exit__(self, *args): self.close()
def sanitize_component(name):
"""
Makes a filename FAT32 safe.
"""
# 1. Transliterate Unicode to ASCII
norm = unicodedata.normalize('NFKD', name)
name = norm.encode('ASCII', 'ignore').decode('ASCII')
# 2. Replace forbidden characters and spaces
name = re.sub(r'[<>:"/\\|?*]', '-', name)
name = re.sub(r'[\000-\037]', '', name)
name = re.sub(r'\s+', '_', name)
# 3. Trim leading/trailing dots and spaces
name = name.strip('. ')
# 4. Ensure not empty
if not name:
name = "_"
# 5. Truncate to MAX_BYTES
encoded = name.encode('utf-8')
if len(encoded) > MAX_BYTES:
name = encoded[:MAX_BYTES].decode('utf-8', 'ignore')
return name
def main():
src_path = Path(SOURCE_DIR)
dst_path = Path(DEST_DIR)
if not src_path.exists():
print(f"Error: Source directory not found: {src_path}")
return
print(f"Source: {src_path}")
print(f"Dest: {dst_path}")
# 1. Count total files for the progress bar
print("Scanning source directory to count files...")
total_files = sum(len(files) for _, _, files in os.walk(src_path))
print(f"Total files found: {total_files}")
print("-" * 40)
# 2. Start the copy process
with tqdm(total=total_files, unit='file') as pbar:
for root, dirs, files in os.walk(src_path):
rel_path = Path(root).relative_to(src_path)
# Sanitize directory path
safe_parts = [sanitize_component(p) for p in rel_path.parts if p != '.']
current_dst_dir = dst_path.joinpath(*safe_parts)
if not current_dst_dir.exists():
try:
current_dst_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
pbar.write(f"Error creating dir {rel_path}: {e}")
continue
for filename in files:
safe_filename = sanitize_component(filename)
src_file = Path(root) / filename
dst_file = current_dst_dir / safe_filename
# Logic: Copy if dest missing OR src is newer
should_copy = False
if not dst_file.exists():
should_copy = True
elif src_file.stat().st_mtime > dst_file.stat().st_mtime:
should_copy = True
if should_copy:
try:
# Update description to show current file being copied
# (Only do this if using real tqdm to avoid clutter in fallback)
if HAS_TQDM:
pbar.set_description(f"Copying {safe_filename[:20]}...")
shutil.copy2(src_file, dst_file)
except Exception as e:
pbar.write(f"Failed to copy {filename}: {e}")
# Advance progress bar
pbar.update(1)
print("Operation complete.")
if __name__ == "__main__":
main()
Comments
0 B
|👍
/👎
0 B
|👍
/👎