Files
renamer/ENGINEERING_GUIDE.md
sHa 60f32a7e8c refactor: Remove old decorators and integrate caching into the new cache subsystem
- Deleted the `renamer.decorators` package, including `caching.py` and `__init__.py`, to streamline the codebase.
- Updated tests to reflect changes in import paths for caching decorators.
- Added a comprehensive changelog to document major refactoring efforts and future plans.
- Introduced an engineering guide detailing architecture, core components, and development setup.
2026-01-02 08:12:28 +00:00

27 KiB

Renamer Engineering Guide

Version: 0.7.0-dev Last Updated: 2026-01-01 Python: 3.11+ Status: Active Development

This is the comprehensive technical reference for the Renamer project. It contains all architectural information, implementation details, development workflows, and AI assistant instructions.


Table of Contents

  1. Project Overview
  2. Architecture
  3. Core Components
  4. Development Setup
  5. Testing Strategy
  6. Code Standards
  7. AI Assistant Instructions
  8. Release Process

Project Overview

Purpose

Renamer is a sophisticated Terminal User Interface (TUI) application for managing, viewing metadata, and renaming media files. Built with Python and the Textual framework.

Dual-Mode Operation:

  • Technical Mode: Detailed technical metadata (video tracks, audio streams, codecs, bitrates)
  • Catalog Mode: Media library catalog view with TMDB integration (posters, ratings, descriptions)

Current Version

  • Version: 0.7.0-dev (in development)
  • Python: 3.11+
  • License: Not specified
  • Repository: /home/sha/bin/renamer

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

Dev Dependencies

  • mypy (≥1.0.0): Type checking

System Requirements

  • Python 3.11 or higher
  • UV package manager (recommended)
  • MediaInfo library (system dependency)

Architecture

Architectural Layers

┌─────────────────────────────────────────┐
│         TUI Layer (Textual)             │
│  app.py, screens.py                     │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│         Service Layer                    │
│  FileTreeService, MetadataService,      │
│  RenameService                          │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│         Extractor Layer                  │
│  MediaExtractor coordinates:            │
│  - FilenameExtractor                    │
│  - MediaInfoExtractor                   │
│  - MetadataExtractor                    │
│  - FileInfoExtractor                    │
│  - TMDBExtractor                        │
│  - DefaultExtractor                     │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│         Formatter Layer                  │
│  FormatterApplier coordinates:         │
│  - DataFormatters (size, duration)      │
│  - TextFormatters (case, style)         │
│  - MarkupFormatters (colors, bold)      │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│      Utility & Cache Layer              │
│  - PatternExtractor                     │
│  - LanguageCodeExtractor                │
│  - FrameClassMatcher                    │
│  - Unified Cache Subsystem              │
└─────────────────────────────────────────┘

Design Patterns

  1. Protocol-Based Architecture: DataExtractor Protocol defines extractor interface
  2. Coordinator Pattern: MediaExtractor coordinates multiple extractors with priority system
  3. Strategy Pattern: Cache key strategies for different data types
  4. Decorator Pattern: @cached_method() for method-level caching
  5. Service Layer: Business logic separated from UI
  6. Dependency Injection: Services receive extractors/formatters as dependencies

Core Components

1. Main Application (renamer/app.py)

Class: RenamerApp(App)

Responsibilities:

  • TUI layout management (split view: file tree + details panel)
  • Keyboard/mouse navigation
  • Command palette integration (Ctrl+P)
  • File operation coordination
  • Efficient tree updates

Key Features:

  • Two command providers: AppCommandProvider, CacheCommandProvider
  • Dual-mode support (technical/catalog)
  • Real-time metadata display

2. Service Layer (renamer/services/)

FileTreeService (file_tree_service.py)

  • Directory scanning and validation
  • Recursive tree building with filtering
  • Media file detection (based on MEDIA_TYPES)
  • Permission error handling
  • Tree node searching by path
  • Directory statistics

MetadataService (metadata_service.py)

  • Thread pool management (ThreadPoolExecutor, configurable workers)
  • Thread-safe operations with Lock
  • Concurrent metadata extraction
  • Active extraction tracking and cancellation
  • Cache integration via decorators
  • Synchronous and asynchronous modes
  • Formatter coordination
  • Error handling with callbacks
  • Context manager support

RenameService (rename_service.py)

  • Proposed name generation from metadata
  • Filename validation and sanitization
  • Invalid character removal (cross-platform)
  • Reserved name checking (Windows compatibility)
  • File conflict detection
  • Atomic rename operations
  • Dry-run mode
  • Callback-based rename with success/error handlers
  • Markup tag stripping

3. Extractor System (renamer/extractors/)

Base Protocol (base.py)

class DataExtractor(Protocol):
    """Defines standard interface for all extractors"""
    def extract_title(self) -> Optional[str]: ...
    def extract_year(self) -> Optional[str]: ...
    # ... 21 methods total

MediaExtractor (extractor.py)

Coordinator class managing priority-based extraction:

Priority Order Examples:

  • Title: TMDB → Metadata → Filename → Default
  • Year: Filename → Default
  • Technical info: MediaInfo → Default
  • File info: FileInfo → Default

Usage:

extractor = MediaExtractor(Path("movie.mkv"))
title = extractor.get("title")  # Tries sources in priority order
year = extractor.get("year", source="Filename")  # Force specific source

Specialized Extractors

  1. FilenameExtractor (filename_extractor.py)

    • Parses metadata from filename patterns
    • Detects year, resolution, source, codecs, edition
    • Uses regex patterns and utility classes
    • Handles Cyrillic normalization
    • Extracts language codes with counts (e.g., "2xUKR_ENG")
  2. MediaInfoExtractor (mediainfo_extractor.py)

    • Uses PyMediaInfo library
    • Extracts detailed track information
    • Provides codec, bitrate, frame rate, resolution
    • Frame class matching with tolerances
  3. MetadataExtractor (metadata_extractor.py)

    • Uses Mutagen library for embedded tags
    • Extracts title, artist, duration
    • Falls back to MIME type detection
    • Handles multiple container formats
  4. FileInfoExtractor (fileinfo_extractor.py)

    • Basic file system information
    • Size, modification time, paths
    • Extension extraction
    • Fast, no external dependencies
  5. TMDBExtractor (tmdb_extractor.py)

    • The Movie Database API integration
    • Fetches title, year, ratings, overview, genres
    • Downloads and caches posters
    • Supports movies and TV shows
    • Rate limiting and error handling
  6. DefaultExtractor (default_extractor.py)

    • Fallback extractor providing default values
    • Returns None or empty collections
    • Safe final fallback in extractor chain

4. Formatter System (renamer/formatters/)

Base Classes (base.py)

  • Formatter: Base ABC with abstract format() method
  • DataFormatter: For data transformations (sizes, durations, dates)
  • TextFormatter: For text transformations (case changes)
  • MarkupFormatter: For visual styling (colors, bold, links)
  • CompositeFormatter: For chaining multiple formatters

FormatterApplier (formatter.py)

Coordinator ensuring correct formatter order:

Order: Data → Text → Markup

Global Ordering:

  1. Data formatters (size, duration, date, track info)
  2. Text formatters (uppercase, lowercase, camelcase)
  3. Markup formatters (bold, colors, dim, underline)

Usage:

formatters = [SizeFormatter.format_size, TextFormatter.bold]
result = FormatterApplier.apply_formatters(1024, formatters)
# Result: bold("1.00 KB")

Specialized Formatters

  • MediaFormatter: Main coordinator, mode-aware (technical/catalog)
  • CatalogFormatter: TMDB data, ratings, genres, poster display
  • TrackFormatter: Video/audio/subtitle track formatting with colors
  • ProposedNameFormatter: Intelligent rename suggestions
  • SizeFormatter: Human-readable file sizes
  • DurationFormatter: Duration in HH:MM:SS
  • DateFormatter: Timestamp formatting
  • ResolutionFormatter: Resolution display
  • ExtensionFormatter: File extension handling
  • SpecialInfoFormatter: Edition/source formatting
  • TextFormatter: Text styling utilities

5. Utility Modules (renamer/utils/)

PatternExtractor (pattern_utils.py)

Centralized regex pattern matching:

  • Movie database ID extraction (TMDB, IMDB, Trakt, TVDB)
  • Year extraction and validation
  • Quality indicator detection
  • Source indicator detection
  • Bracketed content manipulation
  • Position finding for year/quality/source

Example:

extractor = PatternExtractor()
db_info = extractor.extract_movie_db_ids("[tmdbid-12345]")
# Returns: {'type': 'tmdb', 'id': '12345'}

LanguageCodeExtractor (language_utils.py)

Language code processing:

  • Extract from brackets: [UKR_ENG]['ukr', 'eng']
  • Extract standalone codes from filename
  • Handle count patterns: [2xUKR_ENG]
  • Convert to ISO 639-3 codes
  • Skip quality indicators and file extensions
  • Format as language counts: "2ukr,eng"

Example:

extractor = LanguageCodeExtractor()
langs = extractor.extract_from_brackets("[2xUKR_ENG]")
# Returns: ['ukr', 'ukr', 'eng']

FrameClassMatcher (frame_utils.py)

Resolution/frame class matching:

  • Multi-step matching algorithm
  • Height and width tolerance
  • Aspect ratio calculation
  • Scan type detection (progressive/interlaced)
  • Standard resolution checking
  • Nominal height/typical widths lookup

Matching Strategy:

  1. Exact height + width match
  2. Height match with aspect ratio validation
  3. Closest height match
  4. Non-standard quality indicator detection

6. Constants (renamer/constants/)

Modular organization (8 files):

  1. media_constants.py: MEDIA_TYPES - Supported video formats
  2. source_constants.py: SOURCE_DICT - Video source types
  3. frame_constants.py: FRAME_CLASSES, NON_STANDARD_QUALITY_INDICATORS
  4. moviedb_constants.py: MOVIE_DB_DICT - Database identifiers
  5. edition_constants.py: SPECIAL_EDITIONS - Edition types
  6. lang_constants.py: SKIP_WORDS - Words to skip in language detection
  7. year_constants.py: is_valid_year(), dynamic year validation
  8. cyrillic_constants.py: CYRILLIC_TO_ENGLISH - Character mappings

Backward Compatibility: All constants exported via __init__.py

7. Cache Subsystem (renamer/cache/)

Unified, modular architecture:

renamer/cache/
├── __init__.py          # Exports and convenience functions
├── core.py              # Core Cache class (thread-safe with RLock)
├── types.py             # CacheEntry, CacheStats TypedDicts
├── strategies.py        # Cache key generation strategies
├── managers.py          # CacheManager for operations
└── decorators.py        # Enhanced cache decorators

Cache Key Strategies

  • FilepathMethodStrategy: For extractor methods
  • APIRequestStrategy: For API responses
  • SimpleKeyStrategy: For simple prefix+id patterns
  • CustomStrategy: User-defined key generation

Cache Decorators

@cached_method(ttl=3600)  # Method caching
def extract_title(self):
    ...

@cached_api(service="tmdb", ttl=21600)  # API caching
def fetch_movie_data(self, movie_id):
    ...

Cache Manager Operations

  • clear_all(): Remove all cache entries
  • clear_by_prefix(prefix): Clear specific cache type
  • clear_expired(): Remove expired entries
  • get_stats(): Comprehensive statistics
  • clear_file_cache(file_path): Clear cache for specific file
  • compact_cache(): Remove empty directories

Command Palette Integration

Access via Ctrl+P:

  • Cache: View Statistics
  • Cache: Clear All
  • Cache: Clear Extractors / TMDB / Posters
  • Cache: Clear Expired / Compact

Thread Safety

  • All operations protected by threading.RLock
  • Safe for concurrent extractor access
  • Memory cache synchronized with file cache

8. UI Screens (renamer/screens.py)

  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

9. Settings System (renamer/settings.py)

Configuration: ~/.config/renamer/config.json

Options:

{
  "mode": "technical",  // or "catalog"
  "cache_ttl_extractors": 21600,  // 6 hours
  "cache_ttl_tmdb": 21600,         // 6 hours
  "cache_ttl_posters": 2592000     // 30 days
}

Automatic save/load with defaults.


Development Setup

Installation

# Install UV
curl -LsSf https://astral.sh/uv/install.sh | sh

# Clone and sync
cd /home/sha/bin/renamer
uv sync

# Install dev dependencies
uv sync --extra dev

# Run from source
uv run python renamer/main.py [directory]

Development Commands

# Run installed version
uv run renamer [directory]

# Run tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=renamer

# Type checking
uv run mypy renamer/extractors/default_extractor.py

# Version management
uv run bump-version        # Increment patch version
uv run release             # Bump + sync + build

# Build distribution
uv build                   # Create wheel and tarball

# Install as global tool
uv tool install .

Debugging

# Enable formatter logging
FORMATTER_LOG=1 uv run renamer /path/to/directory
# Creates formatter.log with detailed call traces

Testing Strategy

Test Organization

renamer/test/
├── datasets/                    # Test data
│   ├── filenames/
│   │   ├── filename_patterns.json  # 46 test cases
│   │   └── sample_files/           # Legacy reference
│   ├── mediainfo/
│   │   └── frame_class_tests.json  # 25 test cases
│   └── sample_mediafiles/          # Generated (in .gitignore)
├── conftest.py                  # Fixtures and dataset loaders
├── test_cache_subsystem.py      # 18 cache tests
├── test_services.py             # 30+ service tests
├── test_utils.py                # 70+ utility tests
├── test_formatters.py           # 40+ formatter tests
├── test_filename_detection.py   # Comprehensive filename parsing
├── test_filename_extractor.py   # 368 extractor tests
├── test_mediainfo_*.py          # MediaInfo tests
├── test_fileinfo_extractor.py   # File info tests
└── test_metadata_extractor.py   # Metadata tests

Test Statistics

  • Total Tests: 560 (1 skipped)
  • Service Layer: 30+ tests
  • Utilities: 70+ tests
  • Formatters: 40+ tests
  • Extractors: 400+ tests
  • Cache: 18 tests

Sample File Generation

# Generate 46 test files from filename_patterns.json
uv run python renamer/test/fill_sample_mediafiles.py

Test Fixtures

# Load test datasets
patterns = load_filename_patterns()
frame_tests = load_frame_class_tests()
dataset = load_dataset("custom_name")
file_path = get_test_file_path("movie.mkv")

Running Tests

# All tests
uv run pytest

# Specific test file
uv run pytest renamer/test/test_services.py

# With verbose output
uv run pytest -xvs

# With coverage
uv run pytest --cov=renamer --cov-report=html

Code Standards

Python Standards

  • Version: Python 3.11+
  • Style: PEP 8 guidelines
  • Type Hints: Encouraged for all public APIs
  • Docstrings: Google-style format
  • Pathlib: For all file operations
  • Exception Handling: Specific exceptions (no bare except:)

Docstring Format

def example_function(param1: int, param2: str) -> bool:
    """Brief description of function.

    Longer description if needed, explaining behavior,
    edge cases, or important details.

    Args:
        param1: Description of param1
        param2: Description of param2

    Returns:
        Description of return value

    Raises:
        ValueError: When param1 is negative

    Example:
        >>> example_function(5, "test")
        True
    """
    pass

Type Hints

from typing import Optional

# Function type hints
def extract_title(self) -> Optional[str]:
    ...

# Union types (Python 3.10+)
def extract_movie_db(self) -> list[str] | None:
    ...

# Generic types
def extract_tracks(self) -> list[dict]:
    ...

Logging Strategy

Levels:

  • Debug: Language code conversions, metadata reads, MIME detection
  • Warning: Network failures, API errors, MediaInfo parse failures
  • Error: Formatter application failures

Usage:

import logging
logger = logging.getLogger(__name__)

logger.debug(f"Converted {lang_code} to {iso3_code}")
logger.warning(f"TMDB API request failed: {e}")
logger.error(f"Error applying {formatter.__name__}: {e}")

Error Handling

Guidelines:

  • Catch specific exceptions: (LookupError, ValueError, AttributeError)
  • Log all caught exceptions with context
  • Network errors: (requests.RequestException, ValueError)
  • Always close file handles (use context managers)

Example:

try:
    lang_obj = langcodes.Language.get(lang_code.lower())
    return lang_obj.to_alpha3()
except (LookupError, ValueError, AttributeError) as e:
    logger.debug(f"Invalid language code '{lang_code}': {e}")
    return None

Architecture Patterns

  1. Extractor Pattern: Each extractor focuses on one data source
  2. Formatter Pattern: Formatters handle display logic, extractors handle data
  3. Separation of Concerns: Data extraction → formatting → display
  4. Dependency Injection: Extractors and formatters are modular
  5. Configuration Management: Settings class for all config

Best Practices

  • Simplicity: Avoid over-engineering, keep solutions simple
  • Minimal Changes: Only modify what's explicitly requested
  • Validation: Only at system boundaries (user input, external APIs)
  • Trust Internal Code: Don't add unnecessary error handling
  • Delete Unused Code: No backwards-compatibility hacks
  • No Premature Abstraction: Three similar lines > premature abstraction

AI Assistant Instructions

Core Principles

  1. Read Before Modify: Always read files before suggesting modifications
  2. Follow Existing Patterns: Understand established architecture before changes
  3. Test Everything: Run uv run pytest after all changes
  4. Simplicity First: Avoid over-engineering solutions
  5. Document Changes: Update relevant documentation

When Adding Features

  1. Read existing code and understand architecture
  2. Check REFACTORING_PROGRESS.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

When Debugging

  1. Enable formatter logging: FORMATTER_LOG=1
  2. Check cache state (clear if stale data suspected)
  3. Verify file permissions
  4. Test with sample filenames first
  5. Check logs in formatter.log

When Refactoring

  1. Maintain backward compatibility unless explicitly breaking
  2. Update tests to reflect refactored code
  3. Check all formatters (formatting is centralized)
  4. Verify extractor chain (ensure data flow intact)
  5. Run full test suite

Common Pitfalls to Avoid

  • Don't create new files unless absolutely necessary
  • Don't add features beyond what's requested
  • 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
  • Don't use bare except: clauses
  • Don't use command-line tools when specialized tools exist

Tool Usage

  • Read files: Use Read tool, not cat
  • Edit files: Use Edit tool, not sed
  • Write files: Use Write tool, not echo >>
  • Search files: Use Glob tool, not find
  • Search content: Use Grep tool, not grep
  • Run commands: Use Bash tool for terminal operations only

Git Workflow

Commit Standards:

  • Clear, descriptive messages
  • Focus on "why" not "what"
  • One logical change per commit

Commit Message Format:

type: Brief description (imperative mood)

Longer explanation if needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Safety Protocol:

  • NEVER update git config
  • NEVER run destructive commands without explicit request
  • NEVER skip hooks (--no-verify, --no-gpg-sign)
  • NEVER force push to main/master
  • Avoid git commit --amend unless conditions met

Creating Pull Requests

  1. Run git status, git diff, git log to understand changes
  2. Analyze ALL commits that will be included
  3. Draft comprehensive PR summary
  4. Create PR using:
    gh pr create --title "Title" --body "$(cat <<'EOF'
    ## Summary
    - Bullet points of changes
    
    ## Test plan
    - Testing checklist
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    EOF
    )"
    

Release Process

Version Management

Version Scheme: SemVer (MAJOR.MINOR.PATCH)

Commands:

# Bump patch version (0.6.0 → 0.6.1)
uv run bump-version

# Full release process
uv run release  # Bump + sync + build

Release Checklist

  • All tests passing: uv run pytest
  • Type checking passes: uv run mypy renamer/
  • Documentation updated (CHANGELOG.md, README.md)
  • Version bumped in pyproject.toml
  • Dependencies synced: uv sync
  • Build successful: uv build
  • Install test: uv tool install .
  • Manual testing with real media files

Build Artifacts

dist/
├── renamer-0.7.0-py3-none-any.whl    # Wheel distribution
└── renamer-0.7.0.tar.gz              # Source distribution

API Integration

TMDB API

Configuration:

  • API key stored in renamer/secrets.py
  • Base URL: https://api.themoviedb.org/3/
  • Image base URL for poster downloads

Endpoints Used:

  • Search: /search/movie
  • Movie details: /movie/{id}

Rate Limiting: Handled gracefully with error fallback

Caching:

  • API responses cached for 6 hours
  • Posters cached for 30 days
  • Cache location: ~/.cache/renamer/tmdb/, ~/.cache/renamer/posters/

File Operations

Directory Scanning

  • Recursive search for supported video formats
  • File tree representation with hierarchical structure
  • Efficient tree updates on file operations
  • Permission error handling

File Renaming

Process:

  1. Select file in tree
  2. Press r to initiate rename
  3. Review proposed name (current vs proposed)
  4. Confirm with y or cancel with n
  5. Tree updates in-place without full reload

Proposed Name Format:

Title (Year) [Resolution Source Edition].ext

Sanitization:

  • Invalid characters removed (cross-platform)
  • Reserved names checked (Windows compatibility)
  • Markup tags stripped
  • Length validation

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

Keyboard Commands

Key Action
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
m Toggle mode (technical/catalog)
h Show help screen
Ctrl+S Open settings
Ctrl+P Open command palette

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

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

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

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)
  • Phase 1-3 refactoring (2025-12-31 to 2026-01-01)

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
  • 0.6.0: Cache subsystem, service layer, protocols
  • 0.7.0-dev: Complete refactoring (in progress)

Resources

External Documentation

Internal Documentation

  • README.md: User guide and quick start
  • INSTALL.md: Installation methods
  • DEVELOP.md: Developer setup and debugging
  • CHANGELOG.md: Version history and changes
  • REFACTORING_PROGRESS.md: Future refactoring plans
  • ToDo.md: Current task list

Last Updated: 2026-01-01 Maintainer: sha For: AI Assistants and Developers Repository: /home/sha/bin/renamer