jpenguin

music.py

Nov 23rd, 2025 (edited)
1,414
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 4.42 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. import os
  3. import shutil
  4. import unicodedata
  5. import re
  6. from pathlib import Path
  7. import sys
  8.  
  9. # Copy a directory (~/Masic/RythmBox) to a FAT32 formated
  10. # flash drive, named MUSIC, converting all filenames
  11. # This was written with GPT-5 Mini via duck.ai as a bash script,
  12. # Some manual changes and it was fed into Google Gemini
  13. # Gemini then made further optimizations and rewrote it in python
  14. # --- CONFIGURATION ---
  15. HOME = Path.home()
  16. SOURCE_DIR = HOME / "Music/RythmBox"
  17. DEST_DIR = Path("/media") / HOME.name / "MUSIC"
  18. MAX_BYTES = 255
  19.  
  20. # --- PROGRESS BAR SETUP ---
  21. # Try to import tqdm. If missing, create a dummy fallback.
  22. try:
  23.     from tqdm import tqdm
  24.     HAS_TQDM = True
  25. except ImportError:
  26.     HAS_TQDM = False
  27.     print("Tip: Run 'pip3 install tqdm' for a nicer progress bar with ETA.")
  28.  
  29.     # Fallback class to mimic tqdm if library is missing
  30.     class tqdm:
  31.         def __init__(self, total=0, unit='files', **kwargs):
  32.             self.total = total
  33.             self.n = 0
  34.         def update(self, n=1):
  35.             self.n += n
  36.             sys.stdout.write(f"\rProcessed: {self.n}/{self.total}")
  37.             sys.stdout.flush()
  38.         def close(self):
  39.             print("")
  40.         def write(self, msg):
  41.             sys.stdout.write(f"\r{msg}\n")
  42.         def __enter__(self): return self
  43.         def __exit__(self, *args): self.close()
  44.  
  45. def sanitize_component(name):
  46.     """
  47.    Makes a filename FAT32 safe.
  48.    """
  49.     # 1. Transliterate Unicode to ASCII
  50.     norm = unicodedata.normalize('NFKD', name)
  51.     name = norm.encode('ASCII', 'ignore').decode('ASCII')
  52.  
  53.     # 2. Replace forbidden characters and spaces
  54.     name = re.sub(r'[<>:"/\\|?*]', '-', name)
  55.     name = re.sub(r'[\000-\037]', '', name)
  56.     name = re.sub(r'\s+', '_', name)
  57.  
  58.     # 3. Trim leading/trailing dots and spaces
  59.     name = name.strip('. ')
  60.  
  61.     # 4. Ensure not empty
  62.     if not name:
  63.         name = "_"
  64.  
  65.     # 5. Truncate to MAX_BYTES
  66.     encoded = name.encode('utf-8')
  67.     if len(encoded) > MAX_BYTES:
  68.         name = encoded[:MAX_BYTES].decode('utf-8', 'ignore')
  69.  
  70.     return name
  71.  
  72. def main():
  73.     src_path = Path(SOURCE_DIR)
  74.     dst_path = Path(DEST_DIR)
  75.  
  76.     if not src_path.exists():
  77.         print(f"Error: Source directory not found: {src_path}")
  78.         return
  79.  
  80.     print(f"Source: {src_path}")
  81.     print(f"Dest:   {dst_path}")
  82.  
  83.     # 1. Count total files for the progress bar
  84.     print("Scanning source directory to count files...")
  85.     total_files = sum(len(files) for _, _, files in os.walk(src_path))
  86.     print(f"Total files found: {total_files}")
  87.     print("-" * 40)
  88.  
  89.     # 2. Start the copy process
  90.     with tqdm(total=total_files, unit='file') as pbar:
  91.         for root, dirs, files in os.walk(src_path):
  92.             rel_path = Path(root).relative_to(src_path)
  93.  
  94.             # Sanitize directory path
  95.             safe_parts = [sanitize_component(p) for p in rel_path.parts if p != '.']
  96.             current_dst_dir = dst_path.joinpath(*safe_parts)
  97.  
  98.             if not current_dst_dir.exists():
  99.                 try:
  100.                     current_dst_dir.mkdir(parents=True, exist_ok=True)
  101.                 except OSError as e:
  102.                     pbar.write(f"Error creating dir {rel_path}: {e}")
  103.                     continue
  104.  
  105.             for filename in files:
  106.                 safe_filename = sanitize_component(filename)
  107.                 src_file = Path(root) / filename
  108.                 dst_file = current_dst_dir / safe_filename
  109.  
  110.                 # Logic: Copy if dest missing OR src is newer
  111.                 should_copy = False
  112.                 if not dst_file.exists():
  113.                     should_copy = True
  114.                 elif src_file.stat().st_mtime > dst_file.stat().st_mtime:
  115.                     should_copy = True
  116.  
  117.                 if should_copy:
  118.                     try:
  119.                         # Update description to show current file being copied
  120.                         # (Only do this if using real tqdm to avoid clutter in fallback)
  121.                         if HAS_TQDM:
  122.                             pbar.set_description(f"Copying {safe_filename[:20]}...")
  123.  
  124.                         shutil.copy2(src_file, dst_file)
  125.                     except Exception as e:
  126.                         pbar.write(f"Failed to copy {filename}: {e}")
  127.  
  128.                 # Advance progress bar
  129.                 pbar.update(1)
  130.  
  131.     print("Operation complete.")
  132.  
  133. if __name__ == "__main__":
  134.     main()
  135.  
Advertisement
Add Comment
Please, Sign In to add comment