4 Commits

20 changed files with 854 additions and 179 deletions

View File

@@ -4,44 +4,67 @@
This is a Python Terminal User Interface (TUI) application for managing media files. It uses the Textual library to provide a curses-like interface in the terminal. The app allows users to scan directories for video files, display them in a hierarchical tree view, view detailed metadata information including video, audio, and subtitle tracks, and rename files based on intelligent metadata extraction.
**Current Version**: 0.5.10
Key features:
- Recursive directory scanning
- Recursive directory scanning with tree navigation
- Dual-mode display: Technical (codec/track details) and Catalog (TMDB metadata with posters)
- Tree-based file navigation with expand/collapse functionality
- Detailed metadata extraction from multiple sources
- Intelligent file renaming with proposed names
- Multi-source metadata extraction (MediaInfo, filename parsing, embedded tags, TMDB API)
- Intelligent file renaming with proposed names and confirmation
- Settings management with persistent configuration
- Advanced caching system with TTL (6h extractors, 6h TMDB, 30d posters)
- Terminal poster display using rich-pixels
- Color-coded information display
- Keyboard and mouse navigation
- Multiple UI screens (main app, directory selection, help, rename confirmation)
- Multiple UI screens (main app, directory selection, help, rename confirmation, settings)
- Extensible extractor and formatter architecture
- Loading indicators and error handling
- Loading indicators and comprehensive error handling
## Technology Stack
- Python 3.11+
- Textual (TUI framework)
- PyMediaInfo (detailed track information)
- Mutagen (embedded metadata)
- Python-Magic (MIME type detection)
- Langcodes (language code handling)
- UV (package manager)
- Textual ≥6.11.0 (TUI framework)
- PyMediaInfo ≥6.0.0 (detailed track information)
- Mutagen ≥1.47.0 (embedded metadata)
- Python-Magic ≥0.4.27 (MIME type detection)
- Langcodes ≥3.5.1 (language code handling)
- Requests ≥2.31.0 (HTTP client for TMDB API)
- Rich-Pixels ≥1.0.0 (terminal image display)
- Pytest ≥7.0.0 (testing framework)
- UV (package manager and build tool)
## Code Structure
- `main.py`: Main application entry point with argument parsing
- `pyproject.toml`: Project configuration and dependencies (version 0.2.0)
- `renamer/main.py`: Main application entry point with argument parsing
- `pyproject.toml`: Project configuration and dependencies (version 0.5.10)
- `README.md`: User documentation
- `DEVELOP.md`: Developer guide with debugging info
- `INSTALL.md`: Installation instructions
- `CLAUDE.md`: Comprehensive AI assistant reference guide
- `ToDo.md`: Development task tracking
- `AI_AGENT.md`: This file
- `AI_AGENT.md`: This file (AI agent instructions)
- `renamer/`: Main package
- `app.py`: Main Textual application class with tree management and file operations
- `extractor.py`: MediaExtractor class coordinating multiple extractors
- `settings.py`: Settings management with JSON storage
- `cache.py`: File-based caching system with TTL support
- `secrets.py`: API keys and secrets (TMDB)
- `constants.py`: Application constants (media types, sources, resolutions, special editions)
- `screens.py`: Additional UI screens (OpenScreen, HelpScreen, RenameConfirmScreen, SettingsScreen)
- `bump.py`: Version bump utility
- `release.py`: Release automation script
- `extractors/`: Individual extractor classes
- `extractor.py`: MediaExtractor class coordinating all extractors
- `mediainfo_extractor.py`: PyMediaInfo-based extraction
- `filename_extractor.py`: Filename parsing
- `metadata_extractor.py`: Mutagen-based metadata
- `filename_extractor.py`: Filename parsing with regex patterns
- `metadata_extractor.py`: Mutagen-based embedded metadata
- `fileinfo_extractor.py`: Basic file information
- `tmdb_extractor.py`: The Movie Database API integration
- `default_extractor.py`: Fallback extractor
- `formatters/`: Data formatting classes
- `formatter.py`: Base formatter interface
- `media_formatter.py`: Main formatter coordinating display
- `catalog_formatter.py`: Catalog mode formatting with TMDB data
- `proposed_name_formatter.py`: Generates rename suggestions
- `track_formatter.py`: Track information formatting
- `size_formatter.py`: File size formatting
@@ -52,9 +75,17 @@ Key features:
- `extension_formatter.py`: File extension formatting
- `helper_formatter.py`: Helper formatting utilities
- `special_info_formatter.py`: Special edition information
- `constants.py`: Application constants (supported media types)
- `screens.py`: Additional UI screens (OpenScreen, HelpScreen, RenameConfirmScreen)
- `decorators/`: Utility decorators
- `caching.py`: Caching decorator for automatic method caching
- `test/`: Unit tests for extractors
- `test_filename_extractor.py`: Filename parsing tests
- `test_mediainfo_extractor.py`: MediaInfo extraction tests
- `test_mediainfo_frame_class.py`: Frame class detection tests
- `test_fileinfo_extractor.py`: File info tests
- `test_metadata_extractor.py`: Metadata extraction tests
- `test_filename_detection.py`: Filename pattern detection tests
- `filenames.txt`, `test_filenames.txt`: Sample test data
- `test_cases.json`, `test_mediainfo_frame_class.json`: Test fixtures
## Instructions for AI Agents
@@ -113,14 +144,26 @@ The app uses multiple screens for different operations:
- `HelpScreen`: Comprehensive help with key bindings
- `RenameConfirmScreen`: File rename confirmation with error handling
### Completed Major Features
- ✅ Settings management with JSON configuration
- ✅ Mode toggle (technical/catalog)
- ✅ Caching system with TTL support
- ✅ TMDB integration for catalog data
- ✅ Poster display in terminal
- ✅ Settings UI screen
### Future Enhancements
- Metadata editing capabilities
- Batch rename operations
- Configuration file support
- Plugin system for custom extractors/formatters
- Advanced search and filtering
- Undo/redo functionality
- Blue highlighting for changed parts in proposed filename
- Exclude dev commands from distributed package
- Full genre name expansion (currently shows codes)
- Optimized poster quality and display
### Testing
@@ -141,4 +184,16 @@ The app uses multiple screens for different operations:
- Update ToDo.md when completing tasks
- Update version numbers appropriately
## Important Files for AI Assistants
For comprehensive project information, AI assistants should refer to:
1. **CLAUDE.md**: Complete AI assistant reference guide (most comprehensive)
2. **AI_AGENT.md**: This file (concise instructions)
3. **DEVELOP.md**: Developer setup and debugging
4. **ToDo.md**: Current task list and completed items
5. **README.md**: User-facing documentation
This document should be updated as the project evolves.
---
**Last Updated**: 2025-12-31

441
CLAUDE.md Normal file
View File

@@ -0,0 +1,441 @@
# CLAUDE.md - AI Assistant Reference Guide
This document provides comprehensive project information for AI assistants (like Claude) working on the Renamer project.
## Project Overview
**Renamer** is a sophisticated Terminal User Interface (TUI) application for managing, viewing metadata, and renaming media files. Built with Python and the Textual framework, it provides an interactive, curses-like interface for media collection management.
### Current Version
- **Version**: 0.5.10
- **Python**: 3.11+
- **Status**: Active development with media catalog mode features
## Project Purpose
Renamer serves two primary use cases:
1. **Technical Mode**: Detailed technical metadata viewing (video tracks, audio streams, codecs, bitrates)
2. **Catalog Mode**: Media library catalog view with TMDB integration (posters, ratings, descriptions, genres)
## Architecture Overview
### Core Components
#### Main Application (`renamer/app.py`)
- Main `RenamerApp` class inheriting from Textual's `App`
- Manages TUI layout with split view: file tree (left) and details panel (right)
- Handles keyboard/mouse navigation and user commands
- Coordinates file operations and metadata extraction
- Implements efficient tree updates for file renaming
#### Entry Point (`renamer/main.py`)
- Argument parsing for directory selection
- Application initialization and launch
#### Constants (`renamer/constants.py`)
Defines comprehensive dictionaries:
- `MEDIA_TYPES`: Supported video formats (mkv, avi, mov, mp4, etc.)
- `SOURCE_DICT`: Video source types (WEB-DL, BDRip, BluRay, etc.)
- `FRAME_CLASSES`: Resolution classifications (480p-8K)
- `MOVIE_DB_DICT`: Database identifiers (TMDB, IMDB, Trakt, TVDB)
- `SPECIAL_EDITIONS`: Edition types (Director's Cut, Extended, etc.)
### Extractor System (`renamer/extractors/`)
Modular architecture for gathering metadata from multiple sources:
#### Core Extractors
1. **MediaInfoExtractor** (`mediainfo_extractor.py`)
- Uses PyMediaInfo library
- Extracts detailed track information (video, audio, subtitle)
- Provides codec, bitrate, frame rate, resolution data
2. **FilenameExtractor** (`filename_extractor.py`)
- Parses metadata from filename patterns
- Detects year, resolution, source, codecs, edition info
- Uses regex patterns to extract structured data
3. **MetadataExtractor** (`metadata_extractor.py`)
- Reads embedded metadata using Mutagen
- Extracts tags, container format info
4. **FileInfoExtractor** (`fileinfo_extractor.py`)
- Basic file information (size, dates, permissions)
- MIME type detection via python-magic
5. **TMDBExtractor** (`tmdb_extractor.py`)
- The Movie Database API integration
- Fetches title, year, ratings, overview, genres, poster
- Supports movie and TV show data
6. **DefaultExtractor** (`default_extractor.py`)
- Fallback extractor providing minimal data
#### Extractor Coordinator (`extractor.py`)
- `MediaExtractor` class orchestrates all extractors
- Provides unified `get()` interface for data retrieval
- Caching support via decorators
### Formatter System (`renamer/formatters/`)
Transforms raw extracted data into formatted display strings:
#### Specialized Formatters
1. **MediaFormatter** (`media_formatter.py`)
- Main formatter coordinating all format operations
- Mode-aware (technical vs catalog)
- Applies color coding and styling
2. **CatalogFormatter** (`catalog_formatter.py`)
- Formats catalog mode display
- Renders TMDB data, ratings, genres, overview
- Terminal image display for posters (rich-pixels)
3. **TrackFormatter** (`track_formatter.py`)
- Video/audio/subtitle track formatting
- Color-coded track information
4. **ProposedNameFormatter** (`proposed_name_formatter.py`)
- Generates intelligent rename suggestions
- Pattern: `Title (Year) [Resolution Source Edition].ext`
- Sanitizes filenames (removes invalid characters)
5. **Utility Formatters**
- `SizeFormatter`: Human-readable file sizes
- `DateFormatter`: Timestamp formatting
- `DurationFormatter`: Duration in HH:MM:SS
- `ResolutionFormatter`: Resolution display
- `TextFormatter`: Text styling utilities
- `ExtensionFormatter`: File extension handling
- `SpecialInfoFormatter`: Edition/source formatting
- `HelperFormatter`: General formatting helpers
### Settings & Caching
#### Settings System (`renamer/settings.py`)
- JSON configuration stored in `~/.config/renamer/config.json`
- Configurable options:
- `mode`: "technical" or "catalog"
- `cache_ttl_extractors`: 21600s (6 hours)
- `cache_ttl_tmdb`: 21600s (6 hours)
- `cache_ttl_posters`: 2592000s (30 days)
- Automatic save/load with defaults
#### Cache System (`renamer/cache.py`)
- File-based cache with TTL support
- Location: `~/.cache/renamer/`
- Subdirectory organization (tmdb/, posters/, extractors/, general/)
- Supports JSON and pickle serialization
- In-memory cache for performance
- Image caching for TMDB posters
- Automatic expiration and cleanup
#### Caching Decorators (`renamer/decorators/caching.py`)
- `@cached` decorator for automatic method caching
- Integrates with Settings for TTL configuration
### UI Screens (`renamer/screens.py`)
Additional UI screens for user interaction:
1. **OpenScreen**: Directory selection dialog with validation
2. **HelpScreen**: Comprehensive help with key bindings
3. **RenameConfirmScreen**: File rename confirmation with error handling
4. **SettingsScreen**: Settings configuration interface
### Development Tools
#### Version Management (`renamer/bump.py`)
- `bump-version` command
- Auto-increments patch version in `pyproject.toml`
#### Release Automation (`renamer/release.py`)
- `release` command
- Runs: version bump → dependency sync → package build
## Key Features
### Current Features (v0.5.10)
- Recursive directory scanning for video files
- Tree view with expand/collapse navigation
- Dual-mode display (technical/catalog)
- Detailed metadata extraction from multiple sources
- Intelligent file renaming with preview
- TMDB integration with poster display
- Settings configuration UI
- Persistent caching with TTL
- Loading indicators and error handling
- Confirmation dialogs for file operations
- Color-coded information display
- Keyboard and mouse navigation
### Keyboard Commands
- `q`: Quit application
- `o`: Open directory
- `s`: Scan/rescan directory
- `f`: Refresh metadata for selected file
- `r`: Rename file with proposed name
- `p`: Toggle tree expansion
- `h`: Show help screen
- `^p`: Open command palette
- Settings menu via action bar
## Technology Stack
### Core Dependencies
- **textual** (≥6.11.0): TUI framework
- **pymediainfo** (≥6.0.0): Media track analysis
- **mutagen** (≥1.47.0): Embedded metadata
- **python-magic** (≥0.4.27): MIME detection
- **langcodes** (≥3.5.1): Language code handling
- **requests** (≥2.31.0): HTTP for TMDB API
- **rich-pixels** (≥1.0.0): Terminal image display
- **pytest** (≥7.0.0): Testing framework
### System Requirements
- Python 3.11 or higher
- UV package manager (recommended)
- MediaInfo library (system dependency for pymediainfo)
## Development Workflow
### Setup
```bash
# Install UV
curl -LsSf https://astral.sh/uv/install.sh | sh
# Clone and sync
cd /path/to/renamer
uv sync
# Run from source
uv run python renamer/main.py [directory]
```
### Development Commands
```bash
uv run renamer # Run installed version
uv run pytest # Run tests
uv run bump-version # Increment version
uv run release # Build release (bump + sync + build)
uv build # Build wheel/tarball
uv tool install . # Install as global tool
```
### Debugging
```bash
# Enable formatter logging
FORMATTER_LOG=1 uv run renamer /path/to/directory
# Creates formatter.log with detailed call traces
```
### Testing
- Test files in `renamer/test/`
- Sample filenames in `test/filenames.txt` and `test/test_filenames.txt`
- Test cases in `test/test_cases.json`
- Run with: `uv run pytest`
## Code Style & Standards
### Python Standards
- Type hints encouraged
- PEP 8 style guidelines
- Descriptive variable/function names
- Docstrings for classes and functions
- Pathlib for file operations
- Proper exception handling
### Architecture Patterns
- Extractor pattern: Each extractor focuses on one data source
- Formatter pattern: Formatters handle display logic, extractors handle data
- Separation of concerns: Data extraction → formatting → display
- Dependency injection: Extractors and formatters are modular
- Configuration management: Settings class for all config
### Best Practices
- Avoid over-engineering (keep solutions simple)
- Only add features when explicitly requested
- Validate at system boundaries only (user input, external APIs)
- Don't add unnecessary error handling for internal code
- Trust framework guarantees
- Delete unused code completely (no backwards-compat hacks)
## File Operations
### Directory Scanning
- Recursive search for supported video formats
- File tree representation with hierarchical structure
- Efficient tree updates on file operations
### File Renaming
1. Select file in tree
2. Press `r` to initiate rename
3. Review proposed name (shows current vs proposed)
4. Confirm with `y` or cancel with `n`
5. Tree updates in-place without full reload
### Metadata Caching
- First extraction cached for 6 hours
- TMDB data cached for 6 hours
- Posters cached for 30 days
- Force refresh with `f` command
- Cache invalidated on file rename
## API Integration
### TMDB API
- API key stored in `renamer/secrets.py`
- Search endpoint for movie lookup by title/year
- Image base URL for poster downloads
- Handles rate limiting and errors gracefully
- Falls back to filename data if API unavailable
## Project Files
### Documentation
- `README.md`: User-facing documentation
- `AI_AGENT.md`: AI agent instructions (legacy)
- `DEVELOP.md`: Developer guide
- `INSTALL.md`: Installation instructions
- `ToDo.md`: Task tracking
- `CLAUDE.md`: This file (AI assistant reference)
### Configuration
- `pyproject.toml`: Project metadata, dependencies, build config
- `uv.lock`: Locked dependencies
### Build Artifacts
- `dist/`: Built wheels and tarballs
- `build/`: Build intermediates
- `renamer.egg-info/`: Package metadata
## Known Issues & Limitations
### Current Limitations
- TMDB API requires internet connection
- Poster display requires terminal with image support
- Some special characters in filenames need sanitization
- Large directories may have initial scan delay
### Future Enhancements (See ToDo.md)
- Metadata editing capabilities
- Batch rename operations
- Advanced search and filtering
- Undo/redo functionality
- Plugin system for custom extractors/formatters
- Full genre name expansion (currently shows codes)
- Improved poster quality/display optimization
## Contributing Guidelines
### Making Changes
1. Read existing code and understand architecture
2. Check `ToDo.md` for pending tasks
3. Implement features incrementally
4. Test with real media files
5. Ensure backward compatibility
6. Update documentation
7. Update tests as needed
8. Run `uv run release` before committing
### Commit Standards
- Clear, descriptive commit messages
- Focus on "why" not "what"
- One logical change per commit
- Reference related issues/tasks
### Code Review Checklist
- [ ] Follows PEP 8 style
- [ ] Type hints added where appropriate
- [ ] No unnecessary complexity
- [ ] Tests pass (`uv run pytest`)
- [ ] Documentation updated
- [ ] No security vulnerabilities (XSS, injection, etc.)
- [ ] Efficient resource usage (no memory leaks)
## Security Considerations
- Input sanitization for filenames (see `ProposedNameFormatter`)
- No shell command injection risks
- Safe file operations (pathlib, proper error handling)
- TMDB API key should not be committed (stored in `secrets.py`)
- Cache directory permissions should be user-only
## Performance Notes
- In-memory cache reduces repeated extraction overhead
- File cache persists across sessions
- Tree updates optimized for rename operations
- TMDB requests throttled to respect API limits
- Large directory scans use async/await patterns
## Special Notes for AI Assistants
### When Adding Features
1. **Always read relevant files first** - Never modify code you haven't read
2. **Check ToDo.md** - See if feature is already planned
3. **Understand existing patterns** - Follow established architecture
4. **Test with real files** - Use actual media files for testing
5. **Update documentation** - Keep docs in sync with code
### When Debugging
1. **Enable formatter logging** - Use `FORMATTER_LOG=1` for detailed traces
2. **Check cache state** - Clear cache if stale data suspected
3. **Verify file permissions** - Ensure read/write access
4. **Test with sample filenames** - Use test fixtures first
### When Refactoring
1. **Maintain backward compatibility** - Unless explicitly breaking change
2. **Update tests** - Reflect refactored code
3. **Check all formatters** - Formatting is centralized
4. **Verify extractor chain** - Ensure data flow intact
### Common Pitfalls to Avoid
- Don't create new files unless absolutely necessary (edit existing)
- Don't add features beyond what's requested
- Don't over-engineer solutions
- Don't skip testing with real files
- Don't forget to update version number for releases
- Don't commit secrets or API keys
- Don't use deprecated Textual APIs
## Project History
### Evolution
- Started as simple file renamer
- Added metadata extraction (MediaInfo, Mutagen)
- Expanded to TUI with Textual framework
- Added filename parsing intelligence
- Integrated TMDB for catalog mode
- Added settings and caching system
- Implemented poster display with rich-pixels
- Added dual-mode interface (technical/catalog)
### Version Milestones
- 0.2.x: Initial TUI with basic metadata
- 0.3.x: Enhanced extractors and formatters
- 0.4.x: Added TMDB integration
- 0.5.x: Settings, caching, catalog mode, poster display
## Resources
### External Documentation
- [Textual Documentation](https://textual.textualize.io/)
- [PyMediaInfo Documentation](https://pymediainfo.readthedocs.io/)
- [Mutagen Documentation](https://mutagen.readthedocs.io/)
- [TMDB API Documentation](https://developers.themoviedb.org/3)
- [UV Documentation](https://docs.astral.sh/uv/)
### Internal Documentation
- Main README: User guide and quick start
- DEVELOP.md: Developer setup and debugging
- INSTALL.md: Installation methods
- AI_AGENT.md: Legacy AI instructions (historical)
- ToDo.md: Current task list
---
**Last Updated**: 2025-12-31
**For AI Assistant**: Claude (Anthropic)
**Project Maintainer**: sha
**Repository**: `/home/sha/bin/renamer`

View File

@@ -2,6 +2,8 @@
This guide contains information for developers working on the Renamer project.
**Current Version**: 0.5.10
## Development Setup
### Prerequisites
@@ -67,43 +69,85 @@ Enable detailed logging for formatter operations:
FORMATTER_LOG=1 uv run renamer /path/to/directory
```
This creates `formatter.log` with:
This creates `formatter.log` in the current directory with:
- Formatter call sequences and ordering
- Input/output values for each formatter
- Caller information (file and line number)
- Any errors during formatting
- Timestamp for each operation
### Cache Inspection
Cache is stored in `~/.cache/renamer/` with subdirectories:
- `extractors/`: Extractor results cache
- `tmdb/`: TMDB API response cache
- `posters/`: Downloaded poster images
- `general/`: General purpose cache
To clear cache:
```bash
rm -rf ~/.cache/renamer/
```
### Settings Location
Settings are stored in `~/.config/renamer/config.json`:
```json
{
"mode": "technical",
"cache_ttl_extractors": 21600,
"cache_ttl_tmdb": 21600,
"cache_ttl_posters": 2592000
}
```
## Architecture
The application uses a modular architecture:
The application uses a modular architecture with clear separation of concerns:
### Core Application (`renamer/`)
- **app.py**: Main RenamerApp class (Textual App), tree management, file operations
- **main.py**: Entry point with argument parsing
- **constants.py**: Comprehensive constants (media types, sources, resolutions, editions)
- **settings.py**: Settings management with JSON persistence (`~/.config/renamer/`)
- **cache.py**: File-based caching system with TTL support (`~/.cache/renamer/`)
- **secrets.py**: API keys and secrets (TMDB)
### Extractors (`renamer/extractors/`)
- **MediaInfoExtractor**: Extracts detailed track information using PyMediaInfo
- **FilenameExtractor**: Parses metadata from filenames
- **MetadataExtractor**: Extracts embedded metadata using Mutagen
- **FileInfoExtractor**: Provides basic file information
- **DefaultExtractor**: Fallback extractor
- **MediaExtractor**: Main extractor coordinating all others
Data extraction from multiple sources:
- **extractor.py**: MediaExtractor coordinator class
- **mediainfo_extractor.py**: PyMediaInfo for detailed track information
- **filename_extractor.py**: Regex-based filename parsing
- **metadata_extractor.py**: Mutagen for embedded metadata
- **fileinfo_extractor.py**: Basic file information (size, dates, MIME)
- **tmdb_extractor.py**: The Movie Database API integration
- **default_extractor.py**: Fallback extractor
### Formatters (`renamer/formatters/`)
- **MediaFormatter**: Formats extracted data for display
- **ProposedNameFormatter**: Generates intelligent rename suggestions
- **TrackFormatter**: Formats video/audio/subtitle track information
- **SizeFormatter**: Formats file sizes
- **DateFormatter**: Formats timestamps
- **DurationFormatter**: Formats time durations
- **ResolutionFormatter**: Formats video resolutions
- **TextFormatter**: Text styling utilities
Display formatting and rendering:
- **formatter.py**: Base formatter interface
- **media_formatter.py**: Main formatter coordinating all format operations
- **catalog_formatter.py**: Catalog mode display (TMDB data, posters)
- **proposed_name_formatter.py**: Intelligent rename suggestions
- **track_formatter.py**: Video/audio/subtitle track formatting
- **size_formatter.py**: Human-readable file sizes
- **date_formatter.py**: Timestamp formatting
- **duration_formatter.py**: Duration in HH:MM:SS format
- **resolution_formatter.py**: Resolution display
- **extension_formatter.py**: File extension handling
- **special_info_formatter.py**: Edition/source formatting
- **text_formatter.py**: Text styling utilities
- **helper_formatter.py**: General formatting helpers
### Screens (`renamer/screens.py`)
- **OpenScreen**: Directory selection dialog
- **HelpScreen**: Application help and key bindings
- **RenameConfirmScreen**: File rename confirmation dialog
UI screens for user interaction:
- **OpenScreen**: Directory selection with validation
- **HelpScreen**: Comprehensive help with key bindings
- **RenameConfirmScreen**: File rename confirmation with preview
- **SettingsScreen**: Settings configuration UI
### Main Components
- **app.py**: Main TUI application
- **main.py**: Entry point
- **constants.py**: Application constants
### Utilities
- **decorators/caching.py**: Caching decorator for automatic method caching
- **bump.py**: Version bump utility script
- **release.py**: Release automation (bump + sync + build)
## Testing
@@ -133,9 +177,18 @@ uv tool uninstall renamer
## Code Style
The project uses standard Python formatting. Consider using tools like:
The project follows Python best practices:
- **PEP 8**: Standard Python style guide
- **Type Hints**: Encouraged where appropriate
- **Docstrings**: For all classes and public methods
- **Descriptive Naming**: Clear variable and function names
- **Pathlib**: For all file operations
- **Error Handling**: Appropriate exception handling at boundaries
Consider using tools like:
- `ruff` for linting and formatting
- `mypy` for type checking (if added)
- `mypy` for type checking
- `black` for consistent formatting
## Contributing
@@ -146,4 +199,22 @@ The project uses standard Python formatting. Consider using tools like:
5. Run the release process: `uv run release`
6. Submit a pull request
For more information, see the main [README.md](../README.md).
## Additional Documentation
For comprehensive project information:
- **[README.md](README.md)**: User guide and features
- **[CLAUDE.md](CLAUDE.md)**: Complete AI assistant reference
- **[AI_AGENT.md](AI_AGENT.md)**: AI agent instructions
- **[INSTALL.md](INSTALL.md)**: Installation methods
- **[ToDo.md](ToDo.md)**: Task list and priorities
## Project Resources
- **Cache Directory**: `~/.cache/renamer/`
- **Config Directory**: `~/.config/renamer/`
- **Test Files**: `renamer/test/`
- **Build Output**: `dist/` and `build/`
---
**Last Updated**: 2025-12-31

View File

@@ -1,18 +1,23 @@
# Renamer - Media File Renamer and Metadata Editor
# Renamer - Media File Renamer and Metadata Viewer
A terminal-based (TUI) application for scanning directories, viewing media file details, and renaming files based on extracted metadata. Built with Python and Textual.
A powerful terminal-based (TUI) application for managing media collections. Scan directories, view detailed metadata, browse TMDB catalog information with posters, and intelligently rename files. Built with Python and Textual.
**Version**: 0.5.10
## Features
- Recursive directory scanning for video files
- Tree view navigation with keyboard and mouse support
- Detailed metadata extraction from multiple sources (MediaInfo, filename parsing, embedded metadata)
- Intelligent file renaming with proposed names based on metadata
- Color-coded information display
- Command-based interface with hotkeys
- Extensible extractor and formatter system
- Support for video, audio, and subtitle track information
- Confirmation dialogs for file operations
### Core Capabilities
- **Dual Display Modes**: Switch between Technical (codec/track details) and Catalog (TMDB metadata with posters)
- **Recursive Directory Scanning**: Finds all video files in nested directories
- **Tree View Navigation**: Keyboard and mouse support with expand/collapse
- **Multi-Source Metadata**: Combines MediaInfo, filename parsing, embedded tags, and TMDB API
- **Intelligent Renaming**: Proposes standardized names based on extracted metadata
- **Persistent Settings**: Configurable mode and cache TTLs saved to `~/.config/renamer/`
- **Advanced Caching**: File-based cache with TTL (6h extractors, 6h TMDB, 30d posters)
- **Terminal Poster Display**: View movie posters in your terminal using rich-pixels
- **Color-Coded Display**: Visual highlighting for different data types
- **Confirmation Dialogs**: Safe file operations with preview and confirmation
- **Extensible Architecture**: Modular extractor and formatter system for easy extension
## Installation
@@ -48,14 +53,16 @@ renamer
renamer /path/to/media/directory
```
### Commands
### Keyboard Commands
- **q**: Quit the application
- **o**: Open directory selection dialog
- **s**: Rescan current directory
- **f**: Refresh metadata for selected file
- **s**: Scan/rescan current directory
- **f**: Force refresh metadata for selected file (bypass cache)
- **r**: Rename selected file with proposed name
- **p**: Toggle tree expansion (expand/collapse all)
- **h**: Show help screen
- **^p**: Open command palette (settings, mode toggle)
- **Settings**: Access via action bar (top-right corner)
### Navigation
- Use arrow keys to navigate the file tree
@@ -67,9 +74,14 @@ renamer /path/to/media/directory
### File Renaming
1. Select a media file in the tree
2. Press **r** to initiate rename
3. Review the proposed new name
3. Review the proposed new name in the confirmation dialog
4. Press **y** to confirm or **n** to cancel
5. The file will be renamed and the tree updated automatically
5. The file will be renamed and the tree updated automatically (cache invalidated)
### Display Modes
- **Technical Mode**: Shows codec details, bitrates, track information, resolutions
- **Catalog Mode**: Shows TMDB data including title, year, rating, overview, genres, and poster
- Toggle between modes via Settings menu or command palette (^p)
## Development
@@ -88,8 +100,19 @@ For development setup, architecture details, debugging information, and contribu
- .ogv
## Dependencies
- textual: TUI framework
- pymediainfo: Detailed media track information
- mutagen: Embedded metadata extraction
- python-magic: MIME type detection
- langcodes: Language code handling
- **textual** ≥6.11.0: TUI framework
- **pymediainfo** ≥6.0.0: Detailed media track information
- **mutagen** ≥1.47.0: Embedded metadata extraction
- **python-magic** ≥0.4.27: MIME type detection
- **langcodes** ≥3.5.1: Language code handling
- **requests** ≥2.31.0: HTTP client for TMDB API
- **rich-pixels** ≥1.0.0: Terminal image display
- **pytest** ≥7.0.0: Testing framework
### System Requirements
- **Python**: 3.11 or higher
- **MediaInfo Library**: System dependency for pymediainfo
- Ubuntu/Debian: `sudo apt install libmediainfo-dev`
- Fedora/CentOS: `sudo dnf install libmediainfo-devel`
- Arch Linux: `sudo pacman -S libmediainfo`
- macOS/Windows: Automatically handled by pymediainfo

88
ToDo.md
View File

@@ -1,6 +1,8 @@
Project: Media File Renamer and Metadata Editor (Python TUI with Textual)
Project: Media File Renamer and Metadata Viewer (Python TUI with Textual)
TODO Steps:
**Current Version**: 0.5.10
## TODO Steps:
1. ✅ Set up Python project structure with UV package manager
2. ✅ Install dependencies: textual, mutagen, pymediainfo, python-magic, pathlib for file handling
3. ✅ Implement recursive directory scanning for video files (*.mkv, *.avi, *.mov, *.mp4, *.wmv, *.flv, *.webm, etc.)
@@ -24,10 +26,11 @@ TODO Steps:
21. ✅ Add error handling for file operations and metadata extraction
22. 🔄 Implement blue highlighting for changed parts in proposed filename display (show differences between current and proposed names)
23. 🔄 Implement build script to exclude dev commands (bump-version, release) from distributed package
24. Implement metadata editing capabilities (future enhancement)
25. Add batch rename operations (future enhancement)
26. Add configuration file support (future enhancement)
27. Add plugin system for custom extractors/formatters (future enhancement)
24. 📋 Implement metadata editing capabilities (future enhancement)
25. 📋 Add batch rename operations (future enhancement)
26. 📋 Add plugin system for custom extractors/formatters (future enhancement)
27. 📋 Add advanced search and filtering capabilities (future enhancement)
28. 📋 Implement undo/redo functionality for file operations (future enhancement)
---
@@ -60,9 +63,74 @@ TODO Steps:
### Phase 5: Poster Handling and Display
15. ✅ Add poster caching (images in cache dir with 1-month TTL)
16. ✅ Implement terminal image display (research rich-pixels or alternatives, add poster_display.py)
16. ✅ Implement terminal image display (using rich-pixels library)
### Phase 6: Polish and Documentation
17. ✅ Create comprehensive CLAUDE.md for AI assistants
18. ✅ Update all markdown documentation files
19. ✅ Ensure version consistency across all files
### Additional TODOs from Plan
- Retrieve full movie details from TMDB (future)
- Expand genres to full names instead of codes (future)
- Optimize poster quality and display (future)
- 📋 Retrieve full movie details from TMDB (currently basic data only)
- 📋 Expand genres to full names instead of codes (currently shows genre IDs)
- 📋 Optimize poster quality and display (improve image rendering)
- 📋 Add TV show support (currently movie-focused)
- 📋 Implement blue highlighting for filename differences
- 📋 Build script to exclude dev commands from distribution
---
## Recently Completed (v0.5.x)
### Version 0.5.10
- Complete media catalog mode implementation
- TMDB integration with poster display
- Settings system with persistent JSON storage
- Advanced caching with TTL support
- Dual-mode display (technical/catalog)
- Settings UI screen
### Version 0.4.x
- Enhanced extractor system
- TMDB extractor foundation
- Improved formatter architecture
### Version 0.3.x
- Expanded metadata extraction
- Multiple formatter types
- Special edition detection
### Version 0.2.x
- Initial TUI implementation
- Basic metadata extraction
- File tree navigation
- Rename functionality
---
## Development Priorities
### High Priority
1. 🔄 Blue highlighting for filename differences (UX improvement)
2. 🔄 Build script for clean distribution packages
3. 📋 Genre ID to name expansion (TMDB lookup)
### Medium Priority
1. 📋 Batch rename operations
2. 📋 Advanced search/filtering
3. 📋 TV show support
### Low Priority (Future)
1. 📋 Metadata editing
2. 📋 Plugin system
3. 📋 Undo/redo functionality
4. 📋 Configuration profiles
---
**Legend:**
- ✅ Completed
- 🔄 In Progress / Partially Complete
- 📋 Planned / Future Enhancement
**Last Updated**: 2025-12-31

BIN
dist/renamer-0.5.10-py3-none-any.whl vendored Normal file

Binary file not shown.

View File

@@ -1,6 +1,6 @@
[project]
name = "renamer"
version = "0.5.7"
version = "0.6.0"
description = "Terminal-based media file renamer and metadata viewer"
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -17,7 +17,6 @@ from .formatters.proposed_name_formatter import ProposedNameFormatter
from .formatters.text_formatter import TextFormatter
from .formatters.catalog_formatter import CatalogFormatter
from .settings import Settings
from .cache import Cache
# Set up logging conditionally
@@ -25,7 +24,7 @@ if os.getenv('FORMATTER_LOG', '0') == '1':
logging.basicConfig(filename='formatter.log', level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
else:
logging.basicConfig(level=logging.CRITICAL) # Disable logging
logging.basicConfig(level=logging.INFO) # Enable logging for debugging
class RenamerApp(App):
@@ -57,7 +56,6 @@ class RenamerApp(App):
self.scan_dir = Path(scan_dir) if scan_dir else None
self.tree_expanded = False
self.settings = Settings()
self.cache = Cache()
def compose(self) -> ComposeResult:
with Horizontal():
@@ -148,10 +146,9 @@ class RenamerApp(App):
).start()
def _extract_and_show_details(self, file_path: Path):
time.sleep(1) # Minimum delay to show loading
try:
# Initialize extractors and formatters
extractor = MediaExtractor.create(file_path, self.cache, self.settings.get("cache_ttl_extractors"))
extractor = MediaExtractor(file_path)
mode = self.settings.get("mode")
if mode == "technical":
@@ -205,11 +202,6 @@ class RenamerApp(App):
tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node
if node and node.data and isinstance(node.data, Path) and node.data.is_file():
# Clear cache for this file
cache_key_base = str(node.data)
# Invalidate all keys for this file (we can improve this later)
for key in ["title", "year", "source", "extension", "video_tracks", "audio_tracks", "subtitle_tracks"]:
self.cache.invalidate(f"{cache_key_base}_{key}")
self._start_loading_animation()
threading.Thread(
target=self._extract_and_show_details, args=(node.data,)
@@ -240,7 +232,7 @@ class RenamerApp(App):
node = tree.cursor_node
if node and node.data and isinstance(node.data, Path) and node.data.is_file():
# Get the proposed name from the extractor
extractor = MediaExtractor.create(node.data, self.cache, self.settings.get("cache_ttl_extractors"))
extractor = MediaExtractor(node.data)
proposed_formatter = ProposedNameFormatter(extractor)
new_name = str(proposed_formatter)
logging.info(f"Proposed new name: {new_name!r} for file: {node.data}")
@@ -273,11 +265,6 @@ class RenamerApp(App):
"""Update the tree node for a renamed file."""
logging.info(f"update_renamed_file called with old_path={old_path}, new_path={new_path}")
# Clear cache for old file
cache_key_base = str(old_path)
for key in ["title", "year", "source", "extension", "video_tracks", "audio_tracks", "subtitle_tracks"]:
self.cache.invalidate(f"{cache_key_base}_{key}")
tree = self.query_one("#file_tree", Tree)
logging.info(f"Before update: cursor_node.data = {tree.cursor_node.data if tree.cursor_node else None}")

View File

@@ -11,13 +11,16 @@ class Cache:
"""File-based cache with TTL support."""
def __init__(self, cache_dir: Optional[Path] = None):
if cache_dir is None:
# Always use the default cache dir to avoid creating cache in scan dir
cache_dir = Path.home() / ".cache" / "renamer"
self.cache_dir = cache_dir
self.cache_dir.mkdir(parents=True, exist_ok=True)
self._memory_cache = {} # In-memory cache for faster access
def _get_cache_file(self, key: str) -> Path:
"""Get cache file path with hashed filename and subdirs."""
import logging
logging.info(f"Cache _get_cache_file called with key: {key!r}")
# Parse key format: ClassName.method_name.param_hash
if '.' in key:
parts = key.split('.')
@@ -26,12 +29,27 @@ class Cache:
method_name = parts[1]
param_hash = parts[2]
# Use class name as subdir, but if it contains '/', use general to avoid creating nested dirs
if '/' in class_name or '\\' in class_name:
subdir = "general"
subkey = key
file_ext = "json"
else:
subdir = class_name
file_ext = "pkl"
# Use class name as subdir
cache_subdir = self.cache_dir / class_name
cache_subdir = self.cache_dir / subdir
logging.info(f"Cache parsed key, class_name: {class_name!r}, cache_subdir: {cache_subdir!r}")
cache_subdir.mkdir(parents=True, exist_ok=True)
if file_ext == "pkl":
# Use method_name.param_hash as filename
return cache_subdir / f"{method_name}.{param_hash}.pkl"
else:
# Hash the subkey for filename
key_hash = hashlib.md5(subkey.encode('utf-8')).hexdigest()
return cache_subdir / f"{key_hash}.json"
# Fallback for old keys (tmdb_, poster_, etc.)
if key.startswith("tmdb_"):
@@ -40,12 +58,16 @@ class Cache:
elif key.startswith("poster_"):
subdir = "posters"
subkey = key[7:] # Remove "poster_" prefix
elif key.startswith("extractor_"):
subdir = "extractors"
subkey = key[10:] # Remove "extractor_" prefix
else:
subdir = "general"
subkey = key
# Create subdir
cache_subdir = self.cache_dir / subdir
logging.info(f"Cache fallback, subdir: {subdir!r}, cache_subdir: {cache_subdir!r}")
cache_subdir.mkdir(parents=True, exist_ok=True)
# Hash the subkey for filename
@@ -54,6 +76,14 @@ class Cache:
def get(self, key: str) -> Optional[Any]:
"""Get cached value if not expired."""
# Check memory cache first
if key in self._memory_cache:
data = self._memory_cache[key]
if time.time() > data.get('expires', 0):
del self._memory_cache[key]
return None
return data.get('value')
cache_file = self._get_cache_file(key)
if not cache_file.exists():
return None
@@ -67,6 +97,8 @@ class Cache:
cache_file.unlink(missing_ok=True)
return None
# Store in memory cache
self._memory_cache[key] = data
return data.get('value')
except (json.JSONDecodeError, IOError):
# Corrupted, remove
@@ -75,11 +107,14 @@ class Cache:
def set(self, key: str, value: Any, ttl_seconds: int) -> None:
"""Set cached value with TTL."""
cache_file = self._get_cache_file(key)
data = {
'value': value,
'expires': time.time() + ttl_seconds
}
# Store in memory cache
self._memory_cache[key] = data
cache_file = self._get_cache_file(key)
try:
with open(cache_file, 'w') as f:
json.dump(data, f)
@@ -154,6 +189,14 @@ class Cache:
def get_object(self, key: str) -> Optional[Any]:
"""Get pickled object from cache if not expired."""
# Check memory cache first
if key in self._memory_cache:
data = self._memory_cache[key]
if time.time() > data.get('expires', 0):
del self._memory_cache[key]
return None
return data.get('value')
cache_file = self._get_cache_file(key)
if not cache_file.exists():
return None
@@ -167,6 +210,8 @@ class Cache:
cache_file.unlink(missing_ok=True)
return None
# Store in memory cache
self._memory_cache[key] = data
return data.get('value')
except (pickle.PickleError, IOError):
# Corrupted, remove
@@ -175,11 +220,14 @@ class Cache:
def set_object(self, key: str, obj: Any, ttl_seconds: int) -> None:
"""Pickle and cache object with TTL."""
cache_file = self._get_cache_file(key)
data = {
'value': obj,
'expires': time.time() + ttl_seconds
}
# Store in memory cache
self._memory_cache[key] = data
cache_file = self._get_cache_file(key)
try:
with open(cache_file, 'wb') as f:
pickle.dump(data, f)

View File

@@ -31,14 +31,17 @@ def cached_method(ttl_seconds: int = 3600) -> Callable:
# Use instance identifier (file_path for extractors)
instance_id = getattr(self, 'file_path', str(id(self)))
if isinstance(instance_id, Path):
# If instance_id contains path separators, hash it to avoid creating subdirs
if '/' in str(instance_id) or '\\' in str(instance_id):
instance_id = hashlib.md5(str(instance_id).encode('utf-8')).hexdigest()
# Create hash from args and kwargs (excluding self)
# Create hash from args and kwargs only if they exist (excluding self)
if args or kwargs:
param_str = json.dumps((args, kwargs), sort_keys=True, default=str)
param_hash = hashlib.md5(param_str.encode('utf-8')).hexdigest()
cache_key = f"{class_name}.{method_name}.{instance_id}.{param_hash}"
else:
cache_key = f"{class_name}.{method_name}.{instance_id}"
# Try to get from cache
cached_result = _cache.get_object(cache_key)

View File

@@ -5,43 +5,19 @@ from .mediainfo_extractor import MediaInfoExtractor
from .fileinfo_extractor import FileInfoExtractor
from .tmdb_extractor import TMDBExtractor
from .default_extractor import DefaultExtractor
import hashlib
class MediaExtractor:
"""Class to extract various metadata from media files using specialized extractors"""
@classmethod
def create(cls, file_path: Path, cache=None, ttl_seconds: int = 21600):
"""Factory method that returns cached object if available, else creates new."""
cache_key = f"extractor_{hashlib.md5(str(file_path).encode('utf-8')).hexdigest()}"
if cache:
cached_obj = cache.get_object(cache_key)
if cached_obj:
print(f"Loaded MediaExtractor object from cache for {file_path.name}")
return cached_obj
# Create new instance
instance = cls(file_path, cache, ttl_seconds)
# Cache the object
if cache:
cache.set_object(cache_key, instance, ttl_seconds)
print(f"Cached MediaExtractor object for {file_path.name}")
return instance
def __init__(self, file_path: Path, cache=None, ttl_seconds: int = 21600):
def __init__(self, file_path: Path):
self.file_path = file_path
self.cache = cache
self.ttl_seconds = ttl_seconds
self.cache_key = f"file_data_{file_path}"
self.filename_extractor = FilenameExtractor(file_path)
self.metadata_extractor = MetadataExtractor(file_path)
self.mediainfo_extractor = MediaInfoExtractor(file_path)
self.fileinfo_extractor = FileInfoExtractor(file_path)
self.tmdb_extractor = TMDBExtractor(file_path, cache, ttl_seconds)
self.tmdb_extractor = TMDBExtractor(file_path)
self.default_extractor = DefaultExtractor()
# Extractor mapping
@@ -191,15 +167,8 @@ class MediaExtractor:
},
}
# No caching logic here - handled in create() method
def get(self, key: str, source: str | None = None):
"""Get extracted data by key, optionally from specific source"""
print(f"Extracting real data for key '{key}' in {self.file_path.name}")
return self._get_uncached(key, source)
def _get_uncached(self, key: str, source: str | None = None):
"""Original get logic without caching"""
if source:
# Specific source requested - find the extractor and call the method directly
for extractor_name, extractor in self._extractors.items():

View File

@@ -3,30 +3,34 @@ import os
import time
import hashlib
import requests
import logging
from pathlib import Path
from typing import Dict, Optional, Tuple, Any
from ..secrets import TMDB_API_KEY, TMDB_ACCESS_TOKEN
from ..cache import Cache
from ..settings import Settings
class TMDBExtractor:
"""Class to extract TMDB movie information"""
def __init__(self, file_path: Path, cache=None, ttl_seconds: int = 21600):
def __init__(self, file_path: Path):
self.file_path = file_path
self.cache = cache
self.ttl_seconds = ttl_seconds
self.cache = Cache()
self.ttl_seconds = Settings().get("cache_ttl_extractors", 21600)
self._movie_db_info = None
def _get_cached_data(self, cache_key: str) -> Optional[Dict[str, Any]]:
"""Get data from cache if valid"""
if self.cache:
return self.cache.get(f"tmdb_{cache_key}")
return self.cache.get_object(f"tmdb_{cache_key}")
return None
def _set_cached_data(self, cache_key: str, data: Dict[str, Any]):
"""Store data in cache"""
if self.cache:
self.cache.set(f"tmdb_{cache_key}", data, self.ttl_seconds)
self.cache.set_object(f"tmdb_{cache_key}", data, self.ttl_seconds)
def _make_tmdb_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
"""Make a request to TMDB API"""
@@ -56,8 +60,10 @@ class TMDBExtractor:
# Check cache first
cached = self._get_cached_data(cache_key)
if cached is not None:
logging.info(f"TMDB cache hit for search: {title} ({year})")
return cached
logging.info(f"TMDB cache miss for search: {title} ({year}), making request")
params = {'query': title}
if year:
params['year'] = year
@@ -95,8 +101,10 @@ class TMDBExtractor:
# Check cache first
cached = self._get_cached_data(cache_key)
if cached is not None:
logging.info(f"TMDB cache hit for movie details: {movie_id}")
return cached
logging.info(f"TMDB cache miss for movie details: {movie_id}, making request")
result = self._make_tmdb_request(f'/movie/{movie_id}')
if result:
# Cache the result

View File

@@ -74,18 +74,6 @@ class FormatterApplier:
# Sort formatters according to the global order
ordered_formatters = sorted(formatters, key=lambda f: FormatterApplier.FORMATTER_ORDER.index(f) if f in FormatterApplier.FORMATTER_ORDER else len(FormatterApplier.FORMATTER_ORDER))
# Get caller info
frame = inspect.currentframe()
if frame and frame.f_back:
caller = f"{frame.f_back.f_code.co_filename}:{frame.f_back.f_lineno} in {frame.f_back.f_code.co_name}"
else:
caller = "Unknown"
logging.info(f"Caller: {caller}")
logging.info(f"Original formatters: {[f.__name__ if hasattr(f, '__name__') else str(f) for f in formatters]}")
logging.info(f"Ordered formatters: {[f.__name__ if hasattr(f, '__name__') else str(f) for f in ordered_formatters]}")
logging.info(f"Input value: {repr(value)}")
# Apply in the ordered sequence
for formatter in ordered_formatters:
try:
@@ -96,7 +84,6 @@ class FormatterApplier:
logging.error(f"Error applying {formatter.__name__ if hasattr(formatter, '__name__') else str(formatter)}: {e}")
value = "Unknown"
logging.info(f"Final value: {repr(value)}")
return value
@staticmethod

View File

@@ -11,7 +11,7 @@ class ProposedNameFormatter:
"""Initialize with media extractor data"""
self.__order = f"[{extractor.get('order')}] " if extractor.get("order") else ""
self.__title = extractor.get("title") or "Unknown Title"
self.__title = (extractor.get("title") or "Unknown Title").replace("/", "-").replace("\\", "-")
self.__year = DateFormatter.format_year(extractor.get("year"))
self.__source = f" {extractor.get('source')}" if extractor.get("source") else ""
self.__frame_class = extractor.get("frame_class") or None
@@ -26,7 +26,8 @@ class ProposedNameFormatter:
return self.rename_line()
def rename_line(self) -> str:
return f"{self.__order}{self.__title} {self.__year}{self.__special_info}{self.__source} [{self.__frame_class}{self.__hdr},{self.__audio_langs}]{self.__db_info}.{self.__extension}"
result = f"{self.__order}{self.__title} {self.__year}{self.__special_info}{self.__source} [{self.__frame_class}{self.__hdr},{self.__audio_langs}]{self.__db_info}.{self.__extension}"
return result.replace("/", "-").replace("\\", "-")
def rename_line_formatted(self, file_path) -> str:
"""Format the proposed name for display with color"""

View File

@@ -129,8 +129,8 @@ class RenameConfirmScreen(Screen):
def __init__(self, old_path: Path, new_name: str):
super().__init__()
self.old_path = old_path
self.new_name = new_name
self.new_path = old_path.parent / new_name
self.new_name = new_name.replace("/", "-").replace("\\", "-")
self.new_path = old_path.parent / self.new_name
self.was_edited = False
def compose(self):
@@ -167,7 +167,7 @@ Do you want to proceed with renaming?
def on_input_changed(self, event):
if event.input.id == "new_name_input":
self.new_name = event.input.value
self.new_name = event.input.value.replace("/", "-").replace("\\", "-")
self.new_path = self.old_path.parent / self.new_name
self.was_edited = True
# Update the display
@@ -178,12 +178,19 @@ Do you want to proceed with renaming?
def on_button_pressed(self, event):
if event.button.id == "rename":
try:
logging.info(f"Renaming {self.old_path} to {self.new_path}")
self.old_path.rename(self.old_path.with_name(self.new_path)) # type: ignore
logging.info(f"Starting rename: old_path={self.old_path}, new_path={self.new_path}")
logging.info(f"Old file name: {self.old_path.name}")
logging.info(f"New file name: {self.new_name}")
logging.info(f"New path parent: {self.new_path.parent}, Old path parent: {self.old_path.parent}")
if "/" in self.new_name or "\\" in self.new_name:
logging.warning(f"New name contains path separators: {self.new_name}")
self.old_path.rename(self.new_path)
logging.info(f"Rename successful: {self.old_path} -> {self.new_path}")
# Update the tree node
self.app.update_renamed_file(self.old_path, self.old_path.with_name(self.new_path)) # type: ignore
self.app.update_renamed_file(self.old_path, self.new_path) # type: ignore
self.app.pop_screen()
except Exception as e:
logging.error(f"Rename failed: {self.old_path} -> {self.new_path}, error: {str(e)}")
# Show error
content = self.query_one("#confirm_content", Static)
content.update(f"Error renaming file: {str(e)}")
@@ -228,12 +235,19 @@ Do you want to proceed with renaming?
if event.key == "y":
# Trigger rename
try:
logging.info(f"Hotkey renaming {self.old_path} to {self.new_path}")
self.old_path.rename(self.old_path.with_name(self.new_path)) # type: ignore
logging.info(f"Hotkey rename: old_path={self.old_path}, new_path={self.new_path}")
logging.info(f"Old file name: {self.old_path.name}")
logging.info(f"New file name: {self.new_name}")
logging.info(f"New path parent: {self.new_path.parent}, Old path parent: {self.old_path.parent}")
if "/" in self.new_name or "\\" in self.new_name:
logging.warning(f"New name contains path separators: {self.new_name}")
self.old_path.rename(self.new_path)
logging.info(f"Hotkey rename successful: {self.old_path} -> {self.new_path}")
# Update the tree node
self.app.update_renamed_file(self.old_path, self.old_path.with_name(self.new_path)) # type: ignore
self.app.update_renamed_file(self.old_path, self.new_path) # type: ignore
self.app.pop_screen()
except Exception as e:
logging.error(f"Hotkey rename failed: {self.old_path} -> {self.new_path}, error: {str(e)}")
# Show error
content = self.query_one("#confirm_content", Static)
content.update(f"Error renaming file: {str(e)}")

View File

@@ -14,7 +14,7 @@ class Settings:
"cache_ttl_posters": 2592000, # 30 days in seconds
}
def __init__(self, config_dir: Path = None):
def __init__(self, config_dir: Path | None = None):
if config_dir is None:
config_dir = Path.home() / ".config" / "renamer"
self.config_dir = config_dir
@@ -26,7 +26,7 @@ class Settings:
"""Load settings from file, using defaults if file doesn't exist."""
if self.config_file.exists():
try:
with open(self.config_file, 'r') as f:
with open(self.config_file, "r") as f:
data = json.load(f)
# Validate and merge with defaults
for key, default_value in self.DEFAULTS.items():
@@ -46,14 +46,14 @@ class Settings:
"""Save current settings to file."""
try:
self.config_dir.mkdir(parents=True, exist_ok=True)
with open(self.config_file, 'w') as f:
with open(self.config_file, "w") as f:
json.dump(self._settings, f, indent=2)
except IOError as e:
print(f"Error: Could not save settings: {e}")
def get(self, key: str) -> Any:
def get(self, key: str, default: Any = None) -> Any:
"""Get a setting value."""
return self._settings.get(key, self.DEFAULTS.get(key))
return self._settings.get(key, self.DEFAULTS.get(key, default))
def set(self, key: str, value: Any) -> None:
"""Set a setting value and save."""

2
uv.lock generated
View File

@@ -342,7 +342,7 @@ wheels = [
[[package]]
name = "renamer"
version = "0.5.7"
version = "0.5.10"
source = { editable = "." }
dependencies = [
{ name = "langcodes" },