package installer import ( "bufio" "fmt" "os" "strings" "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 } // Open and read the config file file, err := os.Open(configPath) if err != nil { return nil, fmt.Errorf("failed to open config file: %w", err) } defer file.Close() settings := &UserSettings{} // Parse config file line by line scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // Skip comments and empty lines if line == "" || strings.HasPrefix(line, "#") { continue } // Parse key=value pairs parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) // Extract user-configurable settings switch key { case constants.ConfigKeyAudioLanguage: // Parse comma-separated languages settings.AudioLanguages = parseCommaSeparated(value) case constants.ConfigKeySubtitleLanguage: // Parse comma-separated languages settings.SubtitleLanguages = parseCommaSeparated(value) case constants.ConfigKeyHardwareDecoder: // Single value for hardware decoder settings.HardwareDecoder = value case constants.ConfigKeyVideoOutput: // Single value for video output driver settings.VideoOutput = value case constants.ConfigKeyScaleFilter: // Single value for scale filter settings.ScaleFilter = value case constants.ConfigKeyDitherAlgorithm: // Single value for dither algorithm settings.DitherAlgorithm = value } } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("error reading config file: %w", err) } 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 should be called after writing a new config file to restore user preferences 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..." // Read the current config file data, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("failed to read config file: %w", err) } lines := strings.Split(string(data), "\n") var newLines []string // Track which keys have been updated keysUpdated := make(map[string]bool) // First pass: Update existing settings for _, line := range lines { trimmed := strings.TrimSpace(line) // Preserve comments and empty lines if trimmed == "" || strings.HasPrefix(trimmed, "#") { newLines = append(newLines, line) continue } // Check if this is a user-configurable setting parts := strings.SplitN(trimmed, "=", 2) if len(parts) != 2 { newLines = append(newLines, line) continue } key := strings.TrimSpace(parts[0]) switch key { case constants.ConfigKeyAudioLanguage: if len(settings.AudioLanguages) > 0 { newLines = append(newLines, fmt.Sprintf("%s=%s", key, strings.Join(settings.AudioLanguages, ","))) keysUpdated[key] = true log.Info(fmt.Sprintf("Applied audio language settings: %v", settings.AudioLanguages)) } else { newLines = append(newLines, line) } case constants.ConfigKeySubtitleLanguage: if len(settings.SubtitleLanguages) > 0 { newLines = append(newLines, fmt.Sprintf("%s=%s", key, strings.Join(settings.SubtitleLanguages, ","))) keysUpdated[key] = true log.Info(fmt.Sprintf("Applied subtitle language settings: %v", settings.SubtitleLanguages)) } else { newLines = append(newLines, line) } case constants.ConfigKeyHardwareDecoder: if settings.HardwareDecoder != "" { newLines = append(newLines, fmt.Sprintf("%s=%s", key, settings.HardwareDecoder)) keysUpdated[key] = true log.Info(fmt.Sprintf("Applied hardware decoder: %s", settings.HardwareDecoder)) } else { newLines = append(newLines, line) } case constants.ConfigKeyVideoOutput: if settings.VideoOutput != "" { newLines = append(newLines, fmt.Sprintf("%s=%s", key, settings.VideoOutput)) keysUpdated[key] = true log.Info(fmt.Sprintf("Applied video output: %s", settings.VideoOutput)) } else { newLines = append(newLines, line) } case constants.ConfigKeyScaleFilter: if settings.ScaleFilter != "" { newLines = append(newLines, fmt.Sprintf("%s=%s", key, settings.ScaleFilter)) keysUpdated[key] = true log.Info(fmt.Sprintf("Applied scale filter: %s", settings.ScaleFilter)) } else { newLines = append(newLines, line) } case constants.ConfigKeyDitherAlgorithm: if settings.DitherAlgorithm != "" { newLines = append(newLines, fmt.Sprintf("%s=%s", key, settings.DitherAlgorithm)) keysUpdated[key] = true log.Info(fmt.Sprintf("Applied dither algorithm: %s", settings.DitherAlgorithm)) } else { newLines = append(newLines, line) } default: // Keep all other lines as-is newLines = append(newLines, line) } } // Second pass: Add missing settings at the end of the file // (in case the new config file doesn't have these settings) appendMissingSetting := func(key, value string) { if !keysUpdated[key] && value != "" { newLines = append(newLines, "") newLines = append(newLines, fmt.Sprintf("%s=%s", key, value)) cr.outputChan <- fmt.Sprintf(" Added %s=%s", key, value) } } appendMissingSetting(constants.ConfigKeyAudioLanguage, strings.Join(settings.AudioLanguages, ",")) appendMissingSetting(constants.ConfigKeySubtitleLanguage, strings.Join(settings.SubtitleLanguages, ",")) appendMissingSetting(constants.ConfigKeyHardwareDecoder, settings.HardwareDecoder) appendMissingSetting(constants.ConfigKeyVideoOutput, settings.VideoOutput) appendMissingSetting(constants.ConfigKeyScaleFilter, settings.ScaleFilter) appendMissingSetting(constants.ConfigKeyDitherAlgorithm, settings.DitherAlgorithm) // Write the modified config back to file if err := os.WriteFile(configPath, []byte(strings.Join(newLines, "\n")), 0644); err != nil { return fmt.Errorf("failed to write config file: %w", err) } cr.outputChan <- "User settings applied successfully!" cr.outputChan <- strings.Repeat("─", 50) return 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 is called after applying user settings to ensure fresh installs get optimal defaults. func SetRecommendedHWADecoderIfEmpty(configPath string, ostype platform.OSType, gpuBrand string, cr *CommandRunner) error { // Get the recommended hardware decoder recommendedHWA := platform.GetRecommendedHWADecoder(ostype, gpuBrand) log.Info(fmt.Sprintf("SetRecommendedHWADecoderIfEmpty: ostype=%s, gpuBrand=%s, recommendedHWA=%s", ostype, gpuBrand, recommendedHWA)) if recommendedHWA == "" || recommendedHWA == "auto" { // No specific recommendation, keep whatever is there if cr != nil { cr.outputChan <- fmt.Sprintf("No specific hardware decoder recommendation for %s/%s, using default", ostype, gpuBrand) } return nil } // Read the current config file data, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("failed to read config file: %w", err) } // Check if hwdec needs to be set or replaced lines := strings.Split(string(data), "\n") hwdecLineIndex := -1 hwdecCurrentValue := "" modified := false for i, line := range lines { trimmed := strings.TrimSpace(line) // Skip comments and empty lines if trimmed == "" || strings.HasPrefix(trimmed, "#") { continue } // Check if this is the hwdec setting parts := strings.SplitN(trimmed, "=", 2) if len(parts) == 2 { key := strings.TrimSpace(parts[0]) if key == constants.ConfigKeyHardwareDecoder { hwdecLineIndex = i // Get the value, stripping any inline comment value := strings.TrimSpace(parts[1]) // Handle inline comments - split on # and take the first part if idx := strings.Index(value, "#"); idx >= 0 { value = strings.TrimSpace(value[:idx]) } hwdecCurrentValue = value break } } } log.Info(fmt.Sprintf("SetRecommendedHWADecoderIfEmpty: hwdecLineIndex=%d, hwdecCurrentValue=%s", hwdecLineIndex, hwdecCurrentValue)) if hwdecLineIndex >= 0 { // hwdec is already set - only replace if it's "auto" (default fallback) if hwdecCurrentValue == "auto" { lines[hwdecLineIndex] = fmt.Sprintf("%s=%s", constants.ConfigKeyHardwareDecoder, recommendedHWA) modified = true log.Info(fmt.Sprintf("SetRecommendedHWADecoderIfEmpty: Replacing 'auto' with '%s'", recommendedHWA)) } else { log.Info(fmt.Sprintf("SetRecommendedHWADecoderIfEmpty: Keeping existing hwdec value '%s'", hwdecCurrentValue)) } // Otherwise, user has a custom value - leave it alone } else { // hwdec is not set at all - add it lines = append(lines, "") lines = append(lines, fmt.Sprintf("# Platform-specific recommended hardware decoder")) lines = append(lines, fmt.Sprintf("%s=%s", constants.ConfigKeyHardwareDecoder, recommendedHWA)) modified = true log.Info(fmt.Sprintf("SetRecommendedHWADecoderIfEmpty: Adding missing hwdec=%s", recommendedHWA)) } if modified { // Write the modified config back to file if err := os.WriteFile(configPath, []byte(strings.Join(lines, "\n")), 0644); err != nil { return fmt.Errorf("failed to 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 } // 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, gpuBrand string) error { // Install the base config if err := installer.InstallMPVConfigWithOutput(cr); err != nil { return err } // Get the config path configPath, err := GetMPVConfigPath() if err != nil { return fmt.Errorf("failed to get mpv config path: %w", err) } // Set the recommended hardware decoder if not already set if err := SetRecommendedHWADecoderIfEmpty(configPath, ostype, gpuBrand, cr); err != nil { cr.outputChan <- "Warning: Failed to set recommended hardware decoder..." log.Error(fmt.Sprintf("Failed to set recommended hardware decoder: %v", err)) // Don't fail the installation, just log the warning } return nil }