package installer import ( "fmt" "os" "strings" "gitgud.io/mike/mpv-manager/internal/fileops" "gitgud.io/mike/mpv-manager/internal/mpvconf" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" "gitgud.io/mike/mpv-manager/pkg/platform" ) // UserSettings contains settings that should be preserved when overwriting mpv.conf type UserSettings struct { // Language settings AudioLanguages []string SubtitleLanguages []string // Hardware acceleration HardwareDecoder string // Additional settings to preserve (can be expanded as needed) VideoOutput string ScaleFilter string DitherAlgorithm string } // PreserveUserSettings reads the current mpv.conf and extracts user-configurable settings // that should be preserved when overwriting the config file func PreserveUserSettings(configPath string, cr *CommandRunner) (*UserSettings, error) { if cr != nil { cr.outputChan <- "Reading current user settings from mpv.conf..." } // Check if config file exists if _, err := os.Stat(configPath); os.IsNotExist(err) { if cr != nil { cr.outputChan <- "No existing mpv.conf found, will use defaults" } return &UserSettings{}, nil } data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("failed to open config file: %w", err) } values := mpvconf.Parse(string(data)) settings := &UserSettings{ AudioLanguages: parseCommaSeparated(values[constants.ConfigKeyAudioLanguage]), SubtitleLanguages: parseCommaSeparated(values[constants.ConfigKeySubtitleLanguage]), HardwareDecoder: values[constants.ConfigKeyHardwareDecoder], VideoOutput: values[constants.ConfigKeyVideoOutput], ScaleFilter: values[constants.ConfigKeyScaleFilter], DitherAlgorithm: values[constants.ConfigKeyDitherAlgorithm], } if cr != nil { count := countNonEmptySettings(settings) cr.outputChan <- fmt.Sprintf("Preserved %d user setting(s)", count) logUserSettings(settings) } return settings, nil } // ApplyUserSettings applies preserved user settings to the mpv.conf file // This compatibility API shares the renderer used by staged config installation. func ApplyUserSettings(configPath string, settings *UserSettings, cr *CommandRunner) error { if settings == nil || cr == nil { return fmt.Errorf("invalid parameters") } cr.outputChan <- "Applying preserved user settings..." return fileops.WithLock(configPath, func() error { data, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("failed to read config file: %w", err) } content, err := applyUserSettingsContent(data, settings, cr) if err != nil { return err } return fileops.AtomicWritePreserve(configPath, content, constants.FilePermission) }) } // applyUserSettingsContent renders preserved settings into a candidate buffer // without touching the live file. Callers can validate and atomically publish // the complete candidate once. func applyUserSettingsContent(data []byte, settings *UserSettings, cr *CommandRunner) ([]byte, error) { if settings == nil || cr == nil { return nil, fmt.Errorf("invalid parameters") } content := string(data) for _, setting := range []struct{ key, value string }{ {constants.ConfigKeyAudioLanguage, strings.Join(settings.AudioLanguages, ",")}, {constants.ConfigKeySubtitleLanguage, strings.Join(settings.SubtitleLanguages, ",")}, {constants.ConfigKeyHardwareDecoder, settings.HardwareDecoder}, {constants.ConfigKeyVideoOutput, settings.VideoOutput}, {constants.ConfigKeyScaleFilter, settings.ScaleFilter}, {constants.ConfigKeyDitherAlgorithm, settings.DitherAlgorithm}, } { if setting.value != "" { content, _ = mpvconf.Set(content, setting.key, setting.value, true) } } cr.outputChan <- "User settings applied successfully!" return []byte(content), nil } // parseCommaSeparated parses a comma-separated value string into a slice func parseCommaSeparated(value string) []string { parts := strings.Split(value, ",") var result []string for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { result = append(result, trimmed) } } return result } // countNonEmptySettings counts how many settings have non-empty values func countNonEmptySettings(settings *UserSettings) int { count := 0 if len(settings.AudioLanguages) > 0 { count++ } if len(settings.SubtitleLanguages) > 0 { count++ } if settings.HardwareDecoder != "" { count++ } if settings.VideoOutput != "" { count++ } if settings.ScaleFilter != "" { count++ } if settings.DitherAlgorithm != "" { count++ } return count } // logUserSettings logs the preserved settings for debugging func logUserSettings(settings *UserSettings) { if len(settings.AudioLanguages) > 0 { log.Info(fmt.Sprintf("Preserved audio languages: %v", settings.AudioLanguages)) } if len(settings.SubtitleLanguages) > 0 { log.Info(fmt.Sprintf("Preserved subtitle languages: %v", settings.SubtitleLanguages)) } if settings.HardwareDecoder != "" { log.Info(fmt.Sprintf("Preserved hardware decoder: %s", settings.HardwareDecoder)) } if settings.VideoOutput != "" { log.Info(fmt.Sprintf("Preserved video output: %s", settings.VideoOutput)) } if settings.ScaleFilter != "" { log.Info(fmt.Sprintf("Preserved scale filter: %s", settings.ScaleFilter)) } if settings.DitherAlgorithm != "" { log.Info(fmt.Sprintf("Preserved dither algorithm: %s", settings.DitherAlgorithm)) } } // SetRecommendedHWADecoderIfEmpty sets the hardware decoder to the platform-specific // recommended value if the current setting in mpv.conf is empty, missing, or set to "auto". // This compatibility API shares the renderer used by staged config installation. func SetRecommendedHWADecoderIfEmpty(configPath string, ostype platform.OSType, gpuInfo *platform.GPUInfo, cr *CommandRunner) error { return fileops.WithLock(configPath, func() error { data, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("failed to read config file: %w", err) } candidate, modified, recommendedHWA := applyRecommendedHWADecoderContent(data, ostype, gpuInfo) if !modified { return nil } if err := fileops.AtomicWritePreserve(configPath, candidate, constants.FilePermission); err != nil { return fmt.Errorf("failed to atomically write config file: %w", err) } if cr != nil { cr.outputChan <- fmt.Sprintf("Set recommended hardware decoder: %s", recommendedHWA) } log.Info(fmt.Sprintf("Set recommended hardware decoder for fresh install: %s", recommendedHWA)) return nil }) } func applyRecommendedHWADecoderContent(data []byte, ostype platform.OSType, gpuInfo *platform.GPUInfo) ([]byte, bool, string) { // Get the recommended hardware decoder (multi-GPU systems get a fallback chain) gpuBrand := "" if gpuInfo != nil { gpuBrand = gpuInfo.Brand } recommendedHWA := platform.GetRecommendedHWADecoder(ostype, gpuBrand, platform.GetGPUBrands(gpuInfo)...) log.Info(fmt.Sprintf("SetRecommendedHWADecoderIfEmpty: ostype=%s, gpuBrand=%s, recommendedHWA=%s", ostype, gpuBrand, recommendedHWA)) if recommendedHWA == "" || recommendedHWA == "auto" { return append([]byte(nil), data...), false, recommendedHWA } // Only root settings influence defaults; profiles and inline comments stay intact. current, exists := mpvconf.Parse(string(data))[constants.ConfigKeyHardwareDecoder] if exists && current != "auto" && current != "" { return append([]byte(nil), data...), false, recommendedHWA } updated, _ := mpvconf.Set(string(data), constants.ConfigKeyHardwareDecoder, recommendedHWA, true) return []byte(updated), updated != string(data), recommendedHWA } // InstallMPVConfigWithPlatformDefaults installs the MPV config and sets platform-specific // recommended defaults. This wraps InstallMPVConfigWithOutput and adds the recommended // hardware decoder setting for fresh installs. func InstallMPVConfigWithPlatformDefaults(installer *Installer, cr *CommandRunner, ostype platform.OSType, gpuInfo *platform.GPUInfo) error { return installer.installMPVConfigWithOutput(cr, func(candidate []byte) ([]byte, error) { updated, modified, recommended := applyRecommendedHWADecoderContent(candidate, ostype, gpuInfo) if modified && cr != nil { cr.outputChan <- fmt.Sprintf("Set recommended hardware decoder: %s", recommended) } return updated, nil }) } // existingMPVConfig is checked before UI setup can create a minimal config. // Application reinstallation must preserve an existing user's complete file. func existingMPVConfig() (bool, error) { path, err := GetMPVConfigPath() if err != nil { return false, err } return existingMPVConfigAtPath(path) } func existingMPVConfigAtPath(path string) (bool, error) { info, err := os.Stat(path) if os.IsNotExist(err) { return false, nil } if err != nil { return false, fmt.Errorf("inspect existing MPV configuration: %w", err) } if !info.Mode().IsRegular() { return false, fmt.Errorf("MPV configuration is not a regular file: %s", path) } return true, nil }