Ctrl K
ComunidadGuías…

backend/bazarr_migrator.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Bazarr configuration parser + re-exports for the wider migration toolchain.

This module keeps the config-file parsing logic (YAML/INI -> normalized
dict) and re-exports the DB-reading helpers (bazarr_db_reader) and the
data-mapping / preview / apply helpers (bazarr_data_mapper). Callers
continue to import everything from ``bazarr_migrator``:

    from bazarr_migrator import (
        parse_bazarr_config,
        migrate_bazarr_db,
        generate_mapping_report,
        preview_migration,
        apply_migration,
    )
"""

import logging

from bazarr_data_mapper import (  # noqa: F401 — re-exported for back-compat
    _mask_preview,
    apply_migration,
    batch_migrate_bazarr_instances,
    import_bazarr_history,
    map_bazarr_provider_settings,
    planned_config_entries,
    preview_migration,
)
from bazarr_db_reader import (  # noqa: F401 — re-exported for back-compat
    _SENSITIVE_FIELDS,
    _get_table_info,
    _read_blacklist,
    _read_history,
    _read_language_profiles,
    _read_movies,
    _read_settings_table,
    _read_shows,
    _validate_table_name,
    generate_mapping_report,
    migrate_bazarr_db,
)

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Config Parsing
# ---------------------------------------------------------------------------


def parse_bazarr_config(file_content: str, filename: str) -> dict:
    """Parse a Bazarr config file into a normalized dict.

    Supports both YAML (.yaml/.yml) and INI (.ini/.cfg) formats.
    If format cannot be determined from filename, tries YAML first, then INI.

    Args:
        file_content: The raw text content of the config file.
        filename: The original filename (used for format detection).

    Returns:
        Normalized dict with keys: sonarr, radarr, general, raw, warnings.
    """
    if not file_content or not file_content.strip():
        return {
            "sonarr": {},
            "radarr": {},
            "general": {},
            "raw": {},
            "warnings": ["Empty config file"],
        }

    ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""

    if ext in ("yaml", "yml"):
        return _parse_yaml(file_content)
    elif ext in ("ini", "cfg"):
        return _parse_ini(file_content)
    else:
        # Try YAML first, then INI
        result = _parse_yaml(file_content)
        if result.get("raw"):
            return result
        return _parse_ini(file_content)


def _parse_yaml(content: str) -> dict:
    """Parse YAML format Bazarr config."""
    warnings = []
    try:
        import yaml

        data = yaml.safe_load(content)
    except ImportError:
        warnings.append("PyYAML not installed -- cannot parse YAML config")
        return {"sonarr": {}, "radarr": {}, "general": {}, "raw": {}, "warnings": warnings}
    except Exception as exc:
        warnings.append(f"YAML parse error: {exc}")
        return {"sonarr": {}, "radarr": {}, "general": {}, "raw": {}, "warnings": warnings}

    if not isinstance(data, dict):
        warnings.append("YAML content is not a mapping")
        return {"sonarr": {}, "radarr": {}, "general": {}, "raw": {}, "warnings": warnings}

    return _normalize_config(data, warnings)


def _parse_ini(content: str) -> dict:
    """Parse INI format Bazarr config."""
    import configparser

    warnings = []
    parser = configparser.ConfigParser()
    try:
        parser.read_string(content)
    except configparser.Error as exc:
        warnings.append(f"INI parse error: {exc}")
        return {"sonarr": {}, "radarr": {}, "general": {}, "raw": {}, "warnings": warnings}

    # Convert sections to nested dict
    data = {}
    for section in parser.sections():
        data[section] = dict(parser[section])

    return _normalize_config(data, warnings)


def _arr_section(section: dict) -> dict:
    """Carry a Bazarr Sonarr/Radarr section over field by field.

    Bazarr keeps the host (``ip``), ``port``, ``ssl`` and ``base_url`` apart;
    ``bazarr_data_mapper.arr_url`` joins them into the URL Sublarr stores.
    """
    return {
        "url": section.get("ip", ""),
        "port": section.get("port", ""),
        "ssl": section.get("ssl", False),
        "api_key": section.get("apikey", section.get("api_key", "")),
        "base_url": section.get("base_url", ""),
    }


# Bazarr 1.x writes both sections; the legacy opensubtitles.org one is usually
# left empty, so the one that holds credentials wins.
_OPENSUBTITLES_SECTIONS = ("opensubtitlescom", "OpenSubtitlesCom", "opensubtitles", "OpenSubtitles")


def _opensubtitles_section(data: dict) -> dict:
    sections = [data[name] for name in _OPENSUBTITLES_SECTIONS if isinstance(data.get(name), dict)]
    for section in sections:
        if section.get("username") or section.get("apikey"):
            return section
    return sections[0] if sections else {}


def _normalize_config(data: dict, warnings: list) -> dict:
    """Normalize parsed config data into a standard structure.

    Extracts Sonarr, Radarr, and general settings from Bazarr config
    regardless of the original format.
    """
    result = {
        "sonarr": {},
        "radarr": {},
        "general": {},
        "raw": data,
        "warnings": warnings,
    }

    # Extract Sonarr settings (Bazarr uses 'sonarr' section)
    sonarr = data.get("sonarr", data.get("Sonarr", {}))
    if isinstance(sonarr, dict):
        result["sonarr"] = _arr_section(sonarr)

    # Extract Radarr settings
    radarr = data.get("radarr", data.get("Radarr", {}))
    if isinstance(radarr, dict):
        result["radarr"] = _arr_section(radarr)

    # Extract general settings
    general = data.get("general", data.get("General", {}))
    if isinstance(general, dict):
        result["general"] = {
            "source_language": general.get(
                "serie_default_language", general.get("default_language", "")
            ),
            "target_language": general.get("serie_default_hi", ""),
            "use_embedded": general.get(
                "use_embedded_subs", general.get("embedded_subs_show_desired", "")
            ),
            "branch": general.get("branch", ""),
        }

    # Extract subtitle provider settings
    providers_section = _opensubtitles_section(data)
    if providers_section:
        result["general"]["opensubtitles_username"] = providers_section.get("username", "")
        result["general"]["opensubtitles_password"] = providers_section.get("password", "")
        result["general"]["opensubtitles_api_key"] = providers_section.get("apikey", "")

    return result