Compare commits
4 Commits
mediacatal
...
3fbf45083f
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fbf45083f | |||
| 6121311444 | |||
| c4777352e9 | |||
| fe11dc45f1 |
95
AI_AGENT.md
95
AI_AGENT.md
@@ -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
441
CLAUDE.md
Normal 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`
|
||||
123
DEVELOP.md
123
DEVELOP.md
@@ -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
|
||||
65
README.md
65
README.md
@@ -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
88
ToDo.md
@@ -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
BIN
dist/renamer-0.5.10-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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:
|
||||
cache_dir = Path.home() / ".cache" / "renamer"
|
||||
# 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)
|
||||
|
||||
# Use method_name.param_hash as filename
|
||||
return cache_subdir / f"{method_name}.{param_hash}.pkl"
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
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}"
|
||||
# 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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user