diff --git a/CLAUDE.md b/CLAUDE.md index 347a6ed..ec9f818 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,15 +23,3 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Source Code link: `https://github.com/kzuraw/tools/blob/main/html/new-tool.html` - kzuraw.com link: `https://kzuraw.com` 5. Update README.md html tool list - -## Python - -- **`python/`**: Standalone scripts with inline dependencies (PEP 723) -- Use `uv run` for execution (handles dependencies automatically) - -### Adding new python script - -1. Add to `python/` directory -2. Use PEP 723 inline dependency spec -3. Run `uvx ruff format python/` and commit changes -4. Update README.md with usage example diff --git a/README.md b/README.md index ba0a24c..c636cb0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Tools -A collection of web-based and python utility tools. +A collection of web-based utility tools. ## Available Tools @@ -12,24 +12,6 @@ A collection of web-based and python utility tools. - **[Markdown to Rich Text](https://tools.kzuraw.com/html/markdown-to-rich-text.html)** - convert markdown to rich text - **[SVG to React](https://tools.kzuraw.com/html/svg-to-react.html)** - convert SVG to React components with camelCased props -## Python Scripts - -### rename_epubs.py - -Rename epub files to `Author - Title.epub` format using metadata from the epub file - -```bash -uv run https://tools.kzuraw.com/python/rename_epubs.py [--dry-run] -``` - -### rename_invoices.py - -Rename invoice PDFs from `company_name invoice_number.pdf` to `yyyy-mm company_name invoice_number.pdf` format. Uses file creation date and formats the invoice number (removes whitespace, converts `/` to `-`). - -```bash -uv run https://tools.kzuraw.com/python/rename_invoices.py [--dry-run] -``` - ## Deployment This project is automatically deployed to Cloudflare Pages. Any changes pushed to the main branch will be reflected at [https://tools.kzuraw.com/](https://tools.kzuraw.com/). diff --git a/python/rename_epubs.py b/python/rename_epubs.py deleted file mode 100644 index 587846c..0000000 --- a/python/rename_epubs.py +++ /dev/null @@ -1,117 +0,0 @@ -# /// script -# requires-python = ">=3.14" -# dependencies = [ -# "click", -# ] -# /// - -"""Rename epub files to 'Author - Title.epub' format.""" - -import re -import xml.etree.ElementTree as ET -import zipfile -from pathlib import Path - -import click - - -def extract_metadata(epub_path: Path) -> tuple[str | None, str | None]: - """Extract author and title from epub metadata.""" - try: - with zipfile.ZipFile(epub_path, "r") as zf: - # Find the OPF file (contains metadata) - container = zf.read("META-INF/container.xml") - container_root = ET.fromstring(container) - ns = {"c": "urn:oasis:names:tc:opendocument:xmlns:container"} - rootfile = container_root.find(".//c:rootfile", ns) - if rootfile is None: - return None, None - opf_path = rootfile.get("full-path") - if not opf_path: - return None, None - - # Parse OPF for metadata - opf_content = zf.read(opf_path) - opf_root = ET.fromstring(opf_content) - dc = {"dc": "http://purl.org/dc/elements/1.1/"} - - title_el = opf_root.find(".//dc:title", dc) - creator_el = opf_root.find(".//dc:creator", dc) - - title = ( - title_el.text.strip() - if title_el is not None and title_el.text - else None - ) - author = ( - creator_el.text.strip() - if creator_el is not None and creator_el.text - else None - ) - - return author, title - except (zipfile.BadZipFile, ET.ParseError, KeyError) as e: - click.echo(f"Error reading {epub_path.name}: {e}", err=True) - return None, None - except Exception as e: - click.echo(f"Unexpected error reading {epub_path.name}: {e}", err=True) - return None, None - - -def sanitize_filename(name: str) -> str: - """Remove characters that are invalid in filenames.""" - invalid_chars = '<>:"/\\|?*' - for char in invalid_chars: - name = name.replace(char, "") - name = re.sub(r"\s+", " ", name) - return name.strip() - - -@click.command() -@click.argument("folder", type=click.Path(exists=True, file_okay=False, path_type=Path)) -@click.option("--dry-run", is_flag=True, help="Preview changes without renaming") -def main(folder: Path, dry_run: bool): - """Rename epub files in FOLDER to 'Author - Title.epub' format.""" - epubs = list(folder.glob("*.epub")) - - if not epubs: - click.echo("No epub files found.") - return - - renamed_count = 0 - skipped_count = 0 - - for epub_path in epubs: - author, title = extract_metadata(epub_path) - - if not author or not title: - click.echo(f"Skipping {epub_path.name}: missing metadata") - skipped_count += 1 - continue - - new_name = sanitize_filename(f"{author} - {title}.epub") - new_path = folder / new_name - - if new_path == epub_path: - click.echo(f"Already named correctly: {epub_path.name}") - continue - - if dry_run: - click.echo(f"Would rename: {epub_path.name} → {new_name}") - else: - epub_path.rename(new_path) - click.echo(f"Renamed: {epub_path.name} → {new_name}") - renamed_count += 1 - - if renamed_count == 0 and skipped_count == 0: - click.echo("\nNo files needed renaming.") - elif dry_run: - click.echo( - f"\nDry run complete. {renamed_count} file(s) would be renamed, {skipped_count} skipped." - ) - else: - click.echo(f"\nRenamed {renamed_count} file(s), {skipped_count} skipped.") - - -if __name__ == "__main__": - main() diff --git a/python/rename_invoices.py b/python/rename_invoices.py deleted file mode 100644 index 2808b54..0000000 --- a/python/rename_invoices.py +++ /dev/null @@ -1,116 +0,0 @@ -# /// script -# requires-python = ">=3.14" -# dependencies = [ -# "click", -# ] -# /// - -"""Rename invoice PDFs to 'yyyy-mm company_name invoice_number.pdf' format.""" - -import re -from datetime import datetime -from pathlib import Path - -import click - - -def get_file_date(file_path: Path) -> tuple[int, int]: - """Get file creation date (year, month) from file metadata.""" - stat = file_path.stat() - # Try birth time (creation time), fall back to mtime - timestamp = getattr(stat, "st_birthtime", None) or stat.st_mtime - dt = datetime.fromtimestamp(timestamp) - return dt.year, dt.month - - -def format_invoice_number(invoice_num: str) -> str: - """Format invoice number: remove whitespace, convert / to -.""" - result = re.sub(r"\s+", "", invoice_num) - result = result.replace("/", "-") - return result - - -def sanitize_filename(name: str) -> str: - """Remove characters that are invalid in filenames.""" - invalid_chars = '<>:"/\\|?*' - for char in invalid_chars: - name = name.replace(char, "") - name = re.sub(r"\s+", " ", name) - return name.strip() - - -@click.command() -@click.argument("folder", type=click.Path(exists=True, file_okay=False, path_type=Path)) -@click.option("--dry-run", is_flag=True, help="Preview changes without renaming") -def main(folder: Path, dry_run: bool): - """Rename invoice PDFs in FOLDER to 'yyyy-mm company_name invoice_number.pdf' format. - - Expected input format: 'company_name invoice_number.pdf' - Output format: 'yyyy-mm company_name invoice_number.pdf' - - The invoice number will have whitespace removed and / converted to -. - The date is taken from the file creation date. - """ - pdfs = list(folder.glob("*.pdf")) - - if not pdfs: - click.echo("No PDF files found.") - return - - renamed_count = 0 - skipped_count = 0 - - for pdf_path in pdfs: - stem = pdf_path.stem - - # Skip files that already have date prefix (yyyy-mm) - if re.match(r"^\d{4}-\d{2}\s+", stem): - click.echo(f"Already has date prefix: {pdf_path.name}") - continue - - # Parse filename: first word is company, rest is invoice number - parts = stem.split(maxsplit=1) - if len(parts) < 2: - click.echo(f"Skipping {pdf_path.name}: unexpected filename format") - skipped_count += 1 - continue - - company_name = parts[0] - invoice_number = format_invoice_number(parts[1]) - - # Get date from file creation time - year, month = get_file_date(pdf_path) - - # Build new filename - new_stem = f"{year}-{month:02d} {company_name} {invoice_number}" - new_name = sanitize_filename(f"{new_stem}.pdf") - new_path = folder / new_name - - if new_path == pdf_path: - click.echo(f"Already named correctly: {pdf_path.name}") - continue - - if new_path.exists(): - click.echo(f"Skipping {pdf_path.name}: target file already exists") - skipped_count += 1 - continue - - if dry_run: - click.echo(f"Would rename: {pdf_path.name} → {new_name}") - else: - pdf_path.rename(new_path) - click.echo(f"Renamed: {pdf_path.name} → {new_name}") - renamed_count += 1 - - if renamed_count == 0 and skipped_count == 0: - click.echo("\nNo files needed renaming.") - elif dry_run: - click.echo( - f"\nDry run complete. {renamed_count} file(s) would be renamed, {skipped_count} skipped." - ) - else: - click.echo(f"\nRenamed {renamed_count} file(s), {skipped_count} skipped.") - - -if __name__ == "__main__": - main()