"""Compilation functions to transform YAML data to Arr API format.""" import logging from typing import Dict, List, Any, Optional from .mappings import TargetApp, ValueResolver from .utils import load_regex_patterns from ..db.queries.format_renames import is_format_in_renames from ..db.queries.settings import get_language_import_score from .logger import get_import_logger logger = logging.getLogger(__name__) # Cache patterns at module level to avoid reloading _CACHED_PATTERNS = None def get_cached_patterns(): """Get cached regex patterns, loading them once on first access.""" global _CACHED_PATTERNS if _CACHED_PATTERNS is None: _CACHED_PATTERNS = load_regex_patterns() return _CACHED_PATTERNS def compile_format_to_api_structure( format_yaml: Dict[str, Any], arr_type: str ) -> Dict[str, Any]: """ Compile a format from YAML to Arr API structure. Args: format_yaml: Format data from YAML file arr_type: 'radarr' or 'sonarr' Returns: Compiled format ready for API """ target_app = TargetApp.RADARR if arr_type.lower() == 'radarr' else TargetApp.SONARR patterns = get_cached_patterns() compiled = { 'name': format_yaml.get('name', 'Unknown') } # Check if format should be included in renames if is_format_in_renames(format_yaml.get('name', '')): compiled['includeCustomFormatWhenRenaming'] = True # Compile specifications from conditions specifications = [] for condition in format_yaml.get('conditions', []): spec = _compile_condition(condition, patterns, target_app) if spec: specifications.append(spec) compiled['specifications'] = specifications return compiled def _compile_condition( condition: Dict[str, Any], patterns: Dict[str, str], target_app: TargetApp ) -> Optional[Dict[str, Any]]: """Compile a single condition to specification.""" condition_type = condition.get('type') spec = { 'name': condition.get('name', ''), 'negate': condition.get('negate', False), 'required': condition.get('required', False), 'fields': [] } if condition_type in ['release_title', 'release_group', 'edition']: pattern_name = condition.get('pattern') pattern = patterns.get(pattern_name) if not pattern: import_logger = get_import_logger() import_logger.warning(f"Pattern not found: {pattern_name}") return None spec['implementation'] = { 'release_title': 'ReleaseTitleSpecification', 'release_group': 'ReleaseGroupSpecification', 'edition': 'EditionSpecification' }[condition_type] spec['fields'] = [{'name': 'value', 'value': pattern}] elif condition_type == 'source': spec['implementation'] = 'SourceSpecification' value = ValueResolver.get_source(condition.get('source'), target_app) spec['fields'] = [{'name': 'value', 'value': value}] elif condition_type == 'resolution': spec['implementation'] = 'ResolutionSpecification' value = ValueResolver.get_resolution(condition.get('resolution')) spec['fields'] = [{'name': 'value', 'value': value}] elif condition_type == 'indexer_flag': spec['implementation'] = 'IndexerFlagSpecification' value = ValueResolver.get_indexer_flag(condition.get('flag', ''), target_app) spec['fields'] = [{'name': 'value', 'value': value}] elif condition_type == 'quality_modifier': if target_app == TargetApp.SONARR: return None spec['implementation'] = 'QualityModifierSpecification' value = ValueResolver.get_quality_modifier(condition.get('qualityModifier')) spec['fields'] = [{'name': 'value', 'value': value}] elif condition_type == 'size': spec['implementation'] = 'SizeSpecification' spec['fields'] = [ {'name': 'min', 'value': condition.get('minSize', 0)}, {'name': 'max', 'value': condition.get('maxSize', 0)} ] elif condition_type == 'language': spec['implementation'] = 'LanguageSpecification' language_name = condition.get('language', '').lower() try: language_data = ValueResolver.get_language(language_name, target_app, for_profile=False) fields = [{'name': 'value', 'value': language_data['id']}] # Handle exceptLanguage field if present if 'exceptLanguage' in condition: except_value = condition['exceptLanguage'] fields.append({ 'name': 'exceptLanguage', 'value': except_value }) spec['fields'] = fields except Exception: import_logger = get_import_logger() import_logger.warning(f"Language not found: {language_name}") return None elif condition_type == 'release_type': # Only supported in Sonarr if target_app == TargetApp.RADARR: return None spec['implementation'] = 'ReleaseTypeSpecification' value = ValueResolver.get_release_type(condition.get('releaseType')) spec['fields'] = [{'name': 'value', 'value': value}] elif condition_type == 'year': spec['implementation'] = 'YearSpecification' spec['fields'] = [ {'name': 'min', 'value': condition.get('minYear', 0)}, {'name': 'max', 'value': condition.get('maxYear', 0)} ] else: import_logger = get_import_logger() import_logger.warning(f"Unknown condition type: {condition_type}") return None return spec def compile_profile_to_api_structure( profile_yaml: Dict[str, Any], arr_type: str ) -> Dict[str, Any]: """ Compile a profile from YAML to Arr API structure. Args: profile_yaml: Profile data from YAML file arr_type: 'radarr' or 'sonarr' Returns: Compiled profile ready for API """ target_app = TargetApp.RADARR if arr_type.lower() == 'radarr' else TargetApp.SONARR quality_mappings = ValueResolver.get_qualities(target_app) compiled = { 'name': profile_yaml.get('name', 'Unknown') } # Build quality items - following the structure from the working compile/profile_compiler.py items = [] cutoff_id = None used_qualities = set() quality_ids_in_groups = set() # Convert group IDs (negative to positive with offset) def convert_group_id(group_id: int) -> int: if group_id < 0: return 1000 + abs(group_id) return group_id # First pass: gather quality IDs in groups to avoid duplicates for quality_entry in profile_yaml.get('qualities', []): if isinstance(quality_entry, dict) and quality_entry.get('id', 0) < 0: # It's a group for q in quality_entry.get('qualities', []): if isinstance(q, dict): q_name = q.get('name', '') mapped_name = ValueResolver.get_quality_name(q_name, target_app) if mapped_name in quality_mappings: quality_ids_in_groups.add(quality_mappings[mapped_name]['id']) # Second pass: add groups and individual qualities for quality_entry in profile_yaml.get('qualities', []): if isinstance(quality_entry, dict): if quality_entry.get('id', 0) < 0: # It's a group group_id = convert_group_id(quality_entry.get('id', 0)) group_item = { 'id': group_id, 'name': quality_entry.get('name', 'Group'), 'items': [], 'allowed': True } for q in quality_entry.get('qualities', []): if isinstance(q, dict): q_name = q.get('name', '') mapped_name = ValueResolver.get_quality_name(q_name, target_app) if mapped_name in quality_mappings: group_item['items'].append({ 'quality': quality_mappings[mapped_name].copy(), 'items': [], 'allowed': True }) used_qualities.add(mapped_name.upper()) if group_item['items']: items.append(group_item) else: # Individual quality q_name = quality_entry.get('name', '') mapped_name = ValueResolver.get_quality_name(q_name, target_app) if mapped_name in quality_mappings: items.append({ 'quality': quality_mappings[mapped_name].copy(), 'items': [], 'allowed': True }) used_qualities.add(mapped_name.upper()) elif isinstance(quality_entry, str): # Simple quality name string mapped_name = ValueResolver.get_quality_name(quality_entry, target_app) if mapped_name in quality_mappings: items.append({ 'quality': quality_mappings[mapped_name].copy(), 'items': [], 'allowed': True }) used_qualities.add(mapped_name.upper()) # Add all unused qualities as disabled for quality_name, quality_data in quality_mappings.items(): if (quality_name.upper() not in used_qualities and quality_data['id'] not in quality_ids_in_groups): items.append({ 'quality': quality_data.copy(), 'items': [], 'allowed': False }) # Handle cutoff/upgrade_until if 'upgrade_until' in profile_yaml and isinstance(profile_yaml['upgrade_until'], dict): cutoff_id_raw = profile_yaml['upgrade_until'].get('id') cutoff_name = profile_yaml['upgrade_until'].get('name', '') mapped_cutoff_name = ValueResolver.get_quality_name(cutoff_name, target_app) if cutoff_id_raw and cutoff_id_raw < 0: cutoff_id = convert_group_id(cutoff_id_raw) elif mapped_cutoff_name in quality_mappings: cutoff_id = quality_mappings[mapped_cutoff_name]['id'] # Handle language language = profile_yaml.get('language', 'any') if language != 'any' and '_' not in language: # Simple language mode try: language_data = ValueResolver.get_language(language, target_app, for_profile=True) except Exception: language_data = ValueResolver.get_language('any', target_app, for_profile=True) else: # Advanced mode or any language_data = ValueResolver.get_language('any', target_app, for_profile=True) # Build format items (without IDs, those get synced later) format_items = [] # Add language-specific formats for advanced mode if language != 'any' and '_' in language: behavior, language_code = language.split('_', 1) # Get the score from database instead of hardcoding language_score = get_language_import_score() # Use proper capitalization for the language name lang_display = language_code.capitalize() # Handle behaviors: 'must' and 'only' (matching old working logic) if behavior in ['must', 'only']: # Add "Not [Language]" format with score from database not_language_name = f"Not {lang_display}" format_items.append({ 'name': not_language_name, 'score': language_score }) # For 'only' behavior, add additional formats if behavior == 'only': format_items.append({ 'name': f"Not Only {lang_display}", 'score': language_score }) format_items.append({ 'name': f"Not Only {lang_display} (Missing)", 'score': language_score }) # Main custom formats for cf in profile_yaml.get('custom_formats', []): format_items.append({ 'name': cf.get('name'), 'score': cf.get('score', 0) }) # App-specific custom formats app_key = f'custom_formats_{arr_type.lower()}' for cf in profile_yaml.get(app_key, []): format_items.append({ 'name': cf.get('name'), 'score': cf.get('score', 0) }) # Reverse items to match expected order items.reverse() compiled['items'] = items compiled['language'] = language_data compiled['upgradeAllowed'] = profile_yaml.get('upgradesAllowed', True) compiled['minFormatScore'] = profile_yaml.get('minCustomFormatScore', 0) compiled['cutoffFormatScore'] = profile_yaml.get('upgradeUntilScore', 0) compiled['formatItems'] = format_items if cutoff_id is not None: compiled['cutoff'] = cutoff_id # Handle minUpgradeFormatScore with proper default compiled['minUpgradeFormatScore'] = max(1, profile_yaml.get('minScoreIncrement', 1)) return compiled