diff --git a/ToDo.md b/ToDo.md index b8febb1..51e62ab 100644 --- a/ToDo.md +++ b/ToDo.md @@ -60,6 +60,14 @@ This file tracks future feature enhancements and improvements. - Support for fanart/backdrops - Poster cache management UI +- [ ] **Dedicated Poster Window with Real Image Support** + - Create separate panel/window for poster display in catalog mode + - Display actual poster images (not ASCII art) using terminal graphics protocols + - Support for Kitty graphics protocol, iTerm2 inline images, or Sixel + - Configurable poster size with smaller font rendering + - Side-by-side layout: metadata (60%) + poster (40%) + - Higher resolution ASCII art as fallback (100+ chars with extended gradient) + - [ ] **Progress Indicators** - Show scan progress - Batch operation progress bars diff --git a/dist/renamer-0.7.5-py3-none-any.whl b/dist/renamer-0.7.5-py3-none-any.whl new file mode 100644 index 0000000..3ec72b5 Binary files /dev/null and b/dist/renamer-0.7.5-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml index b053869..63dbd1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "renamer" -version = "0.7.4" +version = "0.7.5" description = "Terminal-based media file renamer and metadata viewer" readme = "README.md" requires-python = ">=3.11" diff --git a/renamer/app.py b/renamer/app.py index aaf0186..053d4e0 100644 --- a/renamer/app.py +++ b/renamer/app.py @@ -69,7 +69,7 @@ class AppCommandProvider(Provider): ("scan", "Scan Directory", "Scan current directory for media files (s)"), ("refresh", "Refresh File", "Refresh metadata for selected file (f)"), ("rename", "Rename File", "Rename the selected file (r)"), - ("convert", "Convert AVI to MKV", "Convert AVI file to MKV container with metadata (c)"), + ("convert", "Convert to MKV", "Convert AVI/MPG/MPEG file to MKV container with metadata (c)"), ("delete", "Delete File", "Delete the selected file (d)"), ("toggle_mode", "Toggle Display Mode", "Switch between technical and catalog view (m)"), ("expand", "Toggle Tree Expansion", "Expand or collapse all tree nodes (p)"), @@ -105,7 +105,7 @@ class RenamerApp(App): ("s", "scan", "Scan"), ("f", "refresh", "Refresh"), ("r", "rename", "Rename"), - ("c", "convert", "Convert AVI→MKV"), + ("c", "convert", "Convert to MKV"), ("d", "delete", "Delete"), ("p", "expand", "Toggle Tree"), ("m", "toggle_mode", "Toggle Mode"), @@ -390,7 +390,7 @@ By Category:""" self.notify("Proposed name is the same as current name; no rename needed.", severity="information", timeout=3) async def action_convert(self): - """Convert AVI file to MKV with metadata preservation.""" + """Convert AVI/MPG/MPEG file to MKV with metadata preservation.""" tree = self.query_one("#file_tree", Tree) node = tree.cursor_node @@ -403,7 +403,7 @@ By Category:""" # Check if file can be converted if not conversion_service.can_convert(file_path): - self.notify("Only AVI files can be converted to MKV", severity="error", timeout=3) + self.notify("Only AVI, MPG, and MPEG files can be converted to MKV", severity="error", timeout=3) return # Create extractor for metadata diff --git a/renamer/constants/edition_constants.py b/renamer/constants/edition_constants.py index a162228..021bb46 100644 --- a/renamer/constants/edition_constants.py +++ b/renamer/constants/edition_constants.py @@ -7,7 +7,7 @@ and their aliases for detection in filenames. SPECIAL_EDITIONS = { "Theatrical Cut": ["Theatrical Cut"], "Director's Cut": ["Director's Cut", "Director Cut"], - "Extended Edition": ["Extended Edition", "Ultimate Extended Edition"], + "Extended Cut": ["Extended Cut", "Ultimate Extended Cut", "Extended Edition", "Ultimate Extended Edition"], "Special Edition": ["Special Edition"], "Collector's Edition": ["Collector's Edition"], "Criterion Collection": ["Criterion Collection"], diff --git a/renamer/constants/media_constants.py b/renamer/constants/media_constants.py index 5ce795c..23cddd9 100644 --- a/renamer/constants/media_constants.py +++ b/renamer/constants/media_constants.py @@ -43,4 +43,14 @@ MEDIA_TYPES = { "m4v": {"description": "MPEG-4 video", "meta_type": "MP4", "mime": "video/mp4"}, "3gp": {"description": "3GPP multimedia", "meta_type": "MP4", "mime": "video/3gpp"}, "ogv": {"description": "Ogg Video", "meta_type": "Ogg", "mime": "video/ogg"}, + "mpg": { + "description": "MPEG video", + "meta_type": "MPEG-PS", + "mime": "video/mpeg", + }, + "mpeg": { + "description": "MPEG video", + "meta_type": "MPEG-PS", + "mime": "video/mpeg", + }, } diff --git a/renamer/screens.py b/renamer/screens.py index eb66b2a..41fde92 100644 --- a/renamer/screens.py +++ b/renamer/screens.py @@ -308,6 +308,21 @@ Configure application settings. yield Button("Pseudo", id="poster_pseudo", variant="primary" if settings.get("poster") == "pseudo" else "default") yield Button("Viu", id="poster_viu", variant="primary" if settings.get("poster") == "viu" else "default") + # HEVC quality selection + yield Static("HEVC Encoding Quality (for conversions):", classes="label") + with Horizontal(): + yield Button("CRF 18 (Visually Lossless)", id="hevc_crf_18", variant="primary" if settings.get("hevc_crf") == 18 else "default") + yield Button("CRF 23 (High Quality)", id="hevc_crf_23", variant="primary" if settings.get("hevc_crf") == 23 else "default") + yield Button("CRF 28 (Balanced)", id="hevc_crf_28", variant="primary" if settings.get("hevc_crf") == 28 else "default") + + # HEVC preset selection + yield Static("HEVC Encoding Speed (faster = lower quality/smaller file):", classes="label") + with Horizontal(): + yield Button("Ultrafast", id="hevc_preset_ultrafast", variant="primary" if settings.get("hevc_preset") == "ultrafast" else "default") + yield Button("Veryfast", id="hevc_preset_veryfast", variant="primary" if settings.get("hevc_preset") == "veryfast" else "default") + yield Button("Fast", id="hevc_preset_fast", variant="primary" if settings.get("hevc_preset") == "fast" else "default") + yield Button("Medium", id="hevc_preset_medium", variant="primary" if settings.get("hevc_preset") == "medium" else "default") + # TTL inputs yield Static("Cache TTL - Extractors (hours):", classes="label") yield Input(value=str(settings.get("cache_ttl_extractors") // 3600), id="ttl_extractors", classes="input_field") @@ -348,6 +363,30 @@ Configure application settings. no_btn.variant = "primary" if poster_mode == "no" else "default" pseudo_btn.variant = "primary" if poster_mode == "pseudo" else "default" viu_btn.variant = "primary" if poster_mode == "viu" else "default" + elif event.button.id.startswith("hevc_crf_"): + # Toggle HEVC CRF buttons + crf_value = int(event.button.id.split("_")[-1]) + self.app.settings.set("hevc_crf", crf_value) # type: ignore + # Update button variants + crf18_btn = self.query_one("#hevc_crf_18", Button) + crf23_btn = self.query_one("#hevc_crf_23", Button) + crf28_btn = self.query_one("#hevc_crf_28", Button) + crf18_btn.variant = "primary" if crf_value == 18 else "default" + crf23_btn.variant = "primary" if crf_value == 23 else "default" + crf28_btn.variant = "primary" if crf_value == 28 else "default" + elif event.button.id.startswith("hevc_preset_"): + # Toggle HEVC preset buttons + preset_value = event.button.id.split("_")[-1] + self.app.settings.set("hevc_preset", preset_value) # type: ignore + # Update button variants + ultrafast_btn = self.query_one("#hevc_preset_ultrafast", Button) + veryfast_btn = self.query_one("#hevc_preset_veryfast", Button) + fast_btn = self.query_one("#hevc_preset_fast", Button) + medium_btn = self.query_one("#hevc_preset_medium", Button) + ultrafast_btn.variant = "primary" if preset_value == "ultrafast" else "default" + veryfast_btn.variant = "primary" if preset_value == "veryfast" else "default" + fast_btn.variant = "primary" if preset_value == "fast" else "default" + medium_btn.variant = "primary" if preset_value == "medium" else "default" def save_settings(self): try: @@ -409,7 +448,7 @@ class ConvertConfirmScreen(Screen): def compose(self): from .formatters.text_formatter import TextFormatter - title_text = f"{TextFormatter.bold(TextFormatter.yellow('AVI → MKV CONVERSION'))}" + title_text = f"{TextFormatter.bold(TextFormatter.yellow('MKV CONVERSION'))}" # Build details details_lines = [ @@ -438,83 +477,106 @@ class ConvertConfirmScreen(Screen): details_text = "\n".join(details_lines) - warning_text = f""" -{TextFormatter.bold(TextFormatter.red("Fast remux - streams will be copied without re-encoding"))} -{TextFormatter.yellow("This operation may take a few seconds to minutes depending on file size")} + # Get HEVC CRF from settings + settings = self.app.settings # type: ignore + hevc_crf = settings.get("hevc_crf", 23) -Do you want to proceed with conversion? + info_text = f""" +{TextFormatter.bold('Choose conversion mode:')} + +{TextFormatter.green('Copy Mode')} - Fast remux, no re-encoding (seconds to minutes) +{TextFormatter.yellow(f'HEVC Mode')} - Re-encode to H.265, CRF {hevc_crf} quality (minutes to hours) + {TextFormatter.grey('(Change quality in Settings with Ctrl+S)')} """.strip() with Center(): with Vertical(): yield Static(title_text, id="convert_content", markup=True) yield Static(details_text, id="conversion_details", markup=True) - yield Static(warning_text, id="warning_content", markup=True) + yield Static(info_text, id="info_text", markup=True) with Horizontal(id="buttons"): - yield Button("Convert (y)", id="convert", variant="success") + yield Button("Convert Copy (c)", id="convert_copy", variant="success") + yield Button("Convert HEVC (e)", id="convert_hevc", variant="primary") yield Button("Cancel (n)", id="cancel", variant="error") def on_mount(self): - self.set_focus(self.query_one("#convert")) + self.set_focus(self.query_one("#convert_copy")) def on_button_pressed(self, event): - if event.button.id == "convert": - # Start conversion - app = self.app # type: ignore - app.notify("Starting conversion...", severity="information", timeout=2) + if event.button.id == "convert_copy": + self._do_conversion(encode_hevc=False) + elif event.button.id == "convert_hevc": + self._do_conversion(encode_hevc=True) + elif event.button.id == "cancel": + self.app.pop_screen() # type: ignore - def do_conversion(): - from .services.conversion_service import ConversionService - import threading - import logging + def _do_conversion(self, encode_hevc: bool): + """Start conversion with the specified encoding mode.""" + app = self.app # type: ignore + settings = app.settings - conversion_service = ConversionService() - logging.info(f"Starting conversion of {self.avi_path}") + # Get CRF and preset from settings if using HEVC + crf = settings.get("hevc_crf", 23) if encode_hevc else 18 + preset = settings.get("hevc_preset", "fast") if encode_hevc else "medium" - success, message = conversion_service.convert_avi_to_mkv( - self.avi_path, - extractor=self.extractor - ) + mode_str = f"HEVC CRF {crf} ({preset})" if encode_hevc else "Copy" + app.notify(f"Starting conversion ({mode_str})...", severity="information", timeout=2) - logging.info(f"Conversion result: success={success}, message={message}") - - # Schedule UI updates on the main thread - mkv_path = self.avi_path.with_suffix('.mkv') - - def handle_success(): - logging.info(f"handle_success called: {mkv_path}") - app.notify(f"✓ {message}", severity="information", timeout=5) - logging.info(f"Adding file to tree: {mkv_path}") - app.add_file_to_tree(mkv_path) - logging.info("Conversion success handler completed") - - def handle_error(): - logging.info(f"handle_error called: {message}") - app.notify(f"✗ {message}", severity="error", timeout=10) - logging.info("Conversion error handler completed") - - if success: - logging.info(f"Conversion successful, scheduling UI update for {mkv_path}") - app.call_later(handle_success) - else: - logging.error(f"Conversion failed: {message}") - app.call_later(handle_error) - - # Run conversion in background thread + def do_conversion(): + from .services.conversion_service import ConversionService import threading - threading.Thread(target=do_conversion, daemon=True).start() + import logging - # Close the screen - self.app.pop_screen() # type: ignore - else: - # Cancel - self.app.pop_screen() # type: ignore + conversion_service = ConversionService() + logging.info(f"Starting conversion of {self.avi_path} with encode_hevc={encode_hevc}, crf={crf}, preset={preset}") + logging.info(f"CPU architecture: {conversion_service.cpu_arch}") + + success, message = conversion_service.convert_avi_to_mkv( + self.avi_path, + extractor=self.extractor, + encode_hevc=encode_hevc, + crf=crf, + preset=preset + ) + + logging.info(f"Conversion result: success={success}, message={message}") + + # Schedule UI updates on the main thread + mkv_path = self.avi_path.with_suffix('.mkv') + + def handle_success(): + logging.info(f"handle_success called: {mkv_path}") + app.notify(f"✓ {message}", severity="information", timeout=5) + logging.info(f"Adding file to tree: {mkv_path}") + app.add_file_to_tree(mkv_path) + logging.info("Conversion success handler completed") + + def handle_error(): + logging.info(f"handle_error called: {message}") + app.notify(f"✗ {message}", severity="error", timeout=10) + logging.info("Conversion error handler completed") + + if success: + logging.info(f"Conversion successful, scheduling UI update for {mkv_path}") + app.call_later(handle_success) + else: + logging.error(f"Conversion failed: {message}") + app.call_later(handle_error) + + # Run conversion in background thread + import threading + threading.Thread(target=do_conversion, daemon=True).start() + + # Close the screen + self.app.pop_screen() # type: ignore def on_key(self, event): - if event.key == "y": - # Simulate convert button press - convert_button = self.query_one("#convert") - self.on_button_pressed(type('Event', (), {'button': convert_button})()) + if event.key == "c": + # Copy mode + self._do_conversion(encode_hevc=False) + elif event.key == "e": + # HEVC mode + self._do_conversion(encode_hevc=True) elif event.key == "n" or event.key == "escape": self.app.pop_screen() # type: ignore diff --git a/renamer/services/conversion_service.py b/renamer/services/conversion_service.py index ba2acda..a9306d3 100644 --- a/renamer/services/conversion_service.py +++ b/renamer/services/conversion_service.py @@ -10,6 +10,7 @@ This service manages the process of converting AVI files to MKV container: import logging import subprocess +import platform from pathlib import Path from typing import Optional, List, Dict, Tuple @@ -45,29 +46,148 @@ class ConversionService: def __init__(self): """Initialize the conversion service.""" - logger.debug("ConversionService initialized") + self.cpu_arch = self._detect_cpu_architecture() + logger.debug(f"ConversionService initialized with CPU architecture: {self.cpu_arch}") + + def _detect_cpu_architecture(self) -> str: + """Detect CPU architecture for optimization. + + Returns: + Architecture string: 'x86_64', 'arm64', 'aarch64', or 'unknown' + """ + machine = platform.machine().lower() + + # Try to get more specific CPU info + try: + if machine in ['x86_64', 'amd64']: + # Check for Intel vs AMD + with open('/proc/cpuinfo', 'r') as f: + cpuinfo = f.read().lower() + if 'intel' in cpuinfo or 'xeon' in cpuinfo: + return 'intel_x86_64' + elif 'amd' in cpuinfo: + return 'amd_x86_64' + else: + return 'x86_64' + elif machine in ['arm64', 'aarch64']: + # Check for specific ARM chips + with open('/proc/cpuinfo', 'r') as f: + cpuinfo = f.read().lower() + if 'rk3588' in cpuinfo or 'rockchip' in cpuinfo: + return 'arm64_rk3588' + else: + return 'arm64' + except Exception as e: + logger.debug(f"Could not read /proc/cpuinfo: {e}") + + return machine + + def _get_x265_params(self, preset: str = 'medium') -> str: + """Get optimized x265 parameters based on CPU architecture. + + Args: + preset: Encoding preset (ultrafast, superfast, veryfast, faster, fast, medium, slow) + + Returns: + x265 parameter string optimized for the detected CPU + """ + # Base parameters for quality + base_params = [ + 'profile=main10', + 'level=4.1', + ] + + # CPU-specific optimizations + if self.cpu_arch in ['intel_x86_64', 'amd_x86_64', 'x86_64']: + # Intel Xeon / AMD optimization + # Enable assembly optimizations and threading + cpu_params = [ + 'pools=+', # Enable thread pools + 'frame-threads=4', # Parallel frame encoding (adjust based on cores) + 'lookahead-threads=2', # Lookahead threads + 'asm=auto', # Enable CPU-specific assembly optimizations + ] + + # For faster encoding on servers + if preset in ['ultrafast', 'superfast', 'veryfast', 'faster', 'fast']: + cpu_params.extend([ + 'ref=2', # Fewer reference frames for speed + 'bframes=3', # Fewer B-frames + 'me=1', # Faster motion estimation (DIA) + 'subme=1', # Faster subpixel refinement + 'rd=2', # Faster RD refinement + ]) + else: # medium or slow + cpu_params.extend([ + 'ref=3', + 'bframes=4', + 'me=2', # HEX motion estimation + 'subme=2', + 'rd=3', + ]) + + elif self.cpu_arch in ['arm64_rk3588', 'arm64', 'aarch64']: + # ARM64 / RK3588 optimization + # RK3588 has 4x Cortex-A76 + 4x Cortex-A55 + cpu_params = [ + 'pools=+', + 'frame-threads=4', # Use big cores + 'lookahead-threads=1', # Lighter lookahead for ARM + 'asm=auto', # Enable NEON optimizations + ] + + # ARM is slower, so optimize more aggressively for speed + if preset in ['ultrafast', 'superfast', 'veryfast', 'faster', 'fast']: + cpu_params.extend([ + 'ref=1', # Minimal reference frames + 'bframes=2', + 'me=0', # Full search (faster on ARM) + 'subme=0', + 'rd=1', + 'weightp=0', # Disable weighted prediction for speed + 'weightb=0', + ]) + else: # medium + cpu_params.extend([ + 'ref=2', + 'bframes=3', + 'me=1', + 'subme=1', + 'rd=2', + ]) + + else: + # Generic/unknown architecture - conservative settings + cpu_params = [ + 'pools=+', + 'frame-threads=2', + 'ref=2', + 'bframes=3', + ] + + return ':'.join(base_params + cpu_params) def can_convert(self, file_path: Path) -> bool: - """Check if a file can be converted (is AVI). + """Check if a file can be converted (is AVI, MPG, or MPEG). Args: file_path: Path to the file to check Returns: - True if file is AVI and can be converted + True if file is AVI, MPG, or MPEG and can be converted """ if not file_path.exists() or not file_path.is_file(): return False - return file_path.suffix.lower() == '.avi' + return file_path.suffix.lower() in {'.avi', '.mpg', '.mpeg'} - def find_subtitle_files(self, avi_path: Path) -> List[Path]: - """Find subtitle files near the AVI file. + def find_subtitle_files(self, video_path: Path) -> List[Path]: + """Find subtitle files near the video file. Looks for subtitle files with the same basename in the same directory. Args: - avi_path: Path to the AVI file + video_path: Path to the video file Returns: List of Path objects for found subtitle files @@ -77,8 +197,8 @@ class ConversionService: [Path("/media/movie.srt"), Path("/media/movie.eng.srt")] """ subtitle_files = [] - base_name = avi_path.stem # filename without extension - directory = avi_path.parent + base_name = video_path.stem # filename without extension + directory = video_path.parent # Look for files with same base name and subtitle extensions for sub_ext in self.SUBTITLE_EXTENSIONS: @@ -93,7 +213,7 @@ class ConversionService: if sub_file not in subtitle_files: subtitle_files.append(sub_file) - logger.debug(f"Found {len(subtitle_files)} subtitle files for {avi_path.name}") + logger.debug(f"Found {len(subtitle_files)} subtitle files for {video_path.name}") return subtitle_files def map_audio_languages( @@ -142,35 +262,41 @@ class ConversionService: def build_ffmpeg_command( self, - avi_path: Path, + source_path: Path, mkv_path: Path, audio_languages: List[Optional[str]], - subtitle_files: List[Path] + subtitle_files: List[Path], + encode_hevc: bool = False, + crf: int = 18, + preset: str = 'medium' ) -> List[str]: - """Build ffmpeg command for AVI to MKV conversion. + """Build ffmpeg command for video to MKV conversion. Creates a command that: - - Copies video and audio streams (no re-encoding) + - Copies video and audio streams (no re-encoding) OR + - Encodes video to HEVC with high quality settings - Sets audio language metadata - Includes external subtitle files - Sets MKV title from filename Args: - avi_path: Source AVI file + source_path: Source video file (AVI, MPG, or MPEG) mkv_path: Destination MKV file audio_languages: Language codes for each audio track subtitle_files: List of subtitle files to include + encode_hevc: If True, encode video to HEVC instead of copying + crf: Constant Rate Factor for HEVC (18=visually lossless, 23=high quality default) Returns: List of command arguments for subprocess """ cmd = ['ffmpeg'] - # Add flags to fix timestamp issues in AVI files + # Add flags to fix timestamp issues (particularly for AVI files) cmd.extend(['-fflags', '+genpts']) # Input file - cmd.extend(['-i', str(avi_path)]) + cmd.extend(['-i', str(source_path)]) # Add subtitle files as inputs for sub_file in subtitle_files: @@ -186,8 +312,25 @@ class ConversionService: for i in range(len(subtitle_files)): cmd.extend(['-map', f'{i+1}:s:0']) - # Copy codecs (no re-encoding) - cmd.extend(['-c', 'copy']) + # Video codec settings + if encode_hevc: + # HEVC encoding with CPU-optimized parameters + cmd.extend(['-c:v', 'libx265']) + cmd.extend(['-crf', str(crf)]) + # Use specified preset + cmd.extend(['-preset', preset]) + # 10-bit encoding for better quality (if source supports it) + cmd.extend(['-pix_fmt', 'yuv420p10le']) + # CPU-optimized x265 parameters + x265_params = self._get_x265_params(preset) + cmd.extend(['-x265-params', x265_params]) + # Copy audio streams (no audio re-encoding) + cmd.extend(['-c:a', 'copy']) + # Copy subtitle streams + cmd.extend(['-c:s', 'copy']) + else: + # Copy all streams (no re-encoding) + cmd.extend(['-c', 'copy']) # Set audio language metadata for i, lang in enumerate(audio_languages): @@ -195,7 +338,7 @@ class ConversionService: cmd.extend([f'-metadata:s:a:{i}', f'language={lang}']) # Set title metadata from filename - title = avi_path.stem + title = source_path.stem cmd.extend(['-metadata', f'title={title}']) # Output file @@ -209,28 +352,36 @@ class ConversionService: avi_path: Path, extractor: Optional[MediaExtractor] = None, output_path: Optional[Path] = None, - dry_run: bool = False + dry_run: bool = False, + encode_hevc: bool = False, + crf: int = 18, + preset: str = 'medium' ) -> Tuple[bool, str]: - """Convert AVI file to MKV with metadata preservation. + """Convert AVI/MPG/MPEG file to MKV with metadata preservation. Args: - avi_path: Source AVI file path + avi_path: Source video file path (AVI, MPG, or MPEG) extractor: Optional MediaExtractor (creates new if None) output_path: Optional output path (defaults to same name with .mkv) dry_run: If True, build command but don't execute + encode_hevc: If True, encode video to HEVC instead of copying + crf: Constant Rate Factor for HEVC (18=visually lossless, 23=high quality) + preset: x265 preset (ultrafast, veryfast, faster, fast, medium, slow) Returns: Tuple of (success, message) Example: >>> success, msg = service.convert_avi_to_mkv( - ... Path("/media/movie.avi") + ... Path("/media/movie.avi"), + ... encode_hevc=True, + ... crf=18 ... ) >>> print(msg) """ # Validate input if not self.can_convert(avi_path): - error_msg = f"File is not AVI or doesn't exist: {avi_path}" + error_msg = f"File is not a supported format (AVI/MPG/MPEG) or doesn't exist: {avi_path}" logger.error(error_msg) return False, error_msg @@ -273,7 +424,10 @@ class ConversionService: avi_path, output_path, audio_languages, - subtitle_files + subtitle_files, + encode_hevc, + crf, + preset ) # Dry run mode diff --git a/renamer/settings.py b/renamer/settings.py index 3c0391f..5fd5aa3 100644 --- a/renamer/settings.py +++ b/renamer/settings.py @@ -10,6 +10,8 @@ class Settings: DEFAULTS = { "mode": "technical", # "technical" or "catalog" "poster": "no", # "no", "pseudo", "viu" + "hevc_crf": 23, # HEVC quality: 18=visually lossless, 23=high quality, 28=balanced + "hevc_preset": "fast", # HEVC speed: ultrafast, veryfast, faster, fast, medium, slow "cache_ttl_extractors": 21600, # 6 hours in seconds "cache_ttl_tmdb": 21600, # 6 hours in seconds "cache_ttl_posters": 2592000, # 30 days in seconds diff --git a/uv.lock b/uv.lock index f1288b1..f44da92 100644 --- a/uv.lock +++ b/uv.lock @@ -462,7 +462,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.7.4" +version = "0.7.5" source = { editable = "." } dependencies = [ { name = "langcodes" },