package installer import ( "fmt" "os" "os/exec" "path/filepath" "strings" "time" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" ) // ArchitectureInfo contains architecture details and URLs type ArchitectureInfo struct { Arch string URL string FFmpegURL string } // Find7zip finds an available 7zip executable func Find7zip() string { for _, exe := range constants.SevenZipExecutables { if _, err := exec.LookPath(exe); err == nil { return exe } } return "" } // Check7zipAvailable returns true if 7zip is available func Check7zipAvailable() bool { return Find7zip() != "" } // GetArchitectureInfo returns architecture details for Windows install func GetArchitectureInfo(platformArch string, isV3 bool, releaseInfo ReleaseInfo) (ArchitectureInfo, error) { switch platformArch { case constants.ArchAMD64, constants.ArchX8664: if isV3 { return ArchitectureInfo{ Arch: constants.ArchX8664V3, URL: releaseInfo.Windows.X8664v3.URL, FFmpegURL: releaseInfo.FFmpeg.X8664v3.URL, }, nil } return ArchitectureInfo{ Arch: constants.ArchX8664, URL: releaseInfo.Windows.X8664.URL, FFmpegURL: releaseInfo.FFmpeg.X8664.URL, }, nil case constants.ArchARM64: return ArchitectureInfo{ Arch: constants.ArchAarch64, URL: releaseInfo.Windows.Aarch64.URL, FFmpegURL: releaseInfo.FFmpeg.Aarch64.URL, }, nil default: return ArchitectureInfo{}, fmt.Errorf("unsupported architecture: %s (i686/32-bit builds are no longer available)", platformArch) } } // GetMacOSURL returns the appropriate URL for macOS based on architecture func GetMacOSURL(arch string, isMPV bool, releaseInfo ReleaseInfo) (string, error) { if isMPV { if arch == constants.ArchARM64 { return releaseInfo.MacOS.ARMLatest.URL, nil } return releaseInfo.MacOS.Intel15.URL, nil } // IINA if arch == constants.ArchARM64 { return releaseInfo.IINA.ARM.URL, nil } return releaseInfo.IINA.Intel.URL, nil } // GetHomeDir returns the user's home directory func GetHomeDir() (string, error) { return constants.GetHomeDir() } // GetMPVConfigPath returns the path to mpv.conf (Unix) func GetMPVConfigPath() (string, error) { return constants.GetMPVConfigPathWithHome() } // GetMPVConfigDir returns the path to mpv config directory func GetMPVConfigDir() (string, error) { configPath, err := constants.GetMPVConfigPathWithHome() if err != nil { return "", err } return filepath.Dir(configPath), nil } // GetConfigBackupDir returns the path to config backups directory func GetConfigBackupDir() (string, error) { mpvDir, err := GetMPVConfigDir() if err != nil { return "", err } return filepath.Join(mpvDir, constants.ConfigBackupsDir), nil } // CreateBackup creates a timestamped backup of a file func CreateBackup(filePath string, cr *CommandRunner) error { if _, err := os.Stat(filePath); os.IsNotExist(err) { return nil } if cr != nil { cr.outputChan <- "Existing config found, creating backup..." } fileDir := filepath.Dir(filePath) timestamp := time.Now().Format(constants.BackupTimestampFormat) backupPath := filepath.Join(fileDir, timestamp+constants.BackupFilePrefix) if err := os.Rename(filePath, backupPath); err != nil { if cr != nil { cr.outputChan <- fmt.Sprintf("%s: %v", constants.WarnBackupFailed, err) } return err } if cr != nil { cr.outputChan <- fmt.Sprintf("Backup created: %s", backupPath) } return nil } // CreateFullBackup creates a backup in the conf_backups directory with full timestamp func CreateFullBackup(filePath string, cr *CommandRunner) (string, error) { backupDir, err := GetConfigBackupDir() if err != nil { return "", err } if err := os.MkdirAll(backupDir, constants.DirPermission); err != nil { return "", err } timestamp := time.Now().Format(constants.BackupTimestampFullFormat) backupFileName := timestamp + constants.BackupDirFilePrefix backupPath := filepath.Join(backupDir, backupFileName) if cr != nil { cr.outputChan <- fmt.Sprintf("Creating backup: %s", backupPath) } if err := copyFile(filePath, backupPath, constants.FilePermission); err != nil { return "", err } if cr != nil { cr.outputChan <- "Backup created successfully!" } return backupPath, nil } // RestoreBackup restores a backup file to the config location func RestoreBackup(backupPath, configPath string, cr *CommandRunner) error { // Validate backup path before proceeding if err := config.ValidateBackupPath(backupPath); err != nil { return fmt.Errorf("invalid backup path: %w", err) } if _, err := os.Stat(backupPath); os.IsNotExist(err) { return fmt.Errorf("backup file not found: %s", backupPath) } // Create backup of current config first if _, err := os.Stat(configPath); err == nil { if cr != nil { cr.outputChan <- "Existing config found, creating backup..." } backupOfCurrent := filepath.Join(filepath.Dir(configPath), time.Now().Format(constants.BackupTimestampFormat)+constants.BackupFilePrefix) if err := os.Rename(configPath, backupOfCurrent); err != nil { if cr != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to create backup: %v", err) } } else { if cr != nil { cr.outputChan <- fmt.Sprintf("Current config backed up to: %s", backupOfCurrent) } } } if err := copyFile(backupPath, configPath, constants.FilePermission); err != nil { return err } return nil } // copyFile copies a file from src to dst func copyFile(src, dst string, perm os.FileMode) error { data, err := os.ReadFile(src) if err != nil { return err } if err := os.WriteFile(dst, data, perm); err != nil { return err } return nil } // CommandExists checks if a command is available in PATH func CommandExists(name string) bool { _, err := exec.LookPath(name) return err == nil } // GetPrivilegePrefix returns sudo prefix if needed for Linux func GetPrivilegePrefix() []string { if os.Geteuid() == 0 { return []string{} } if CommandExists(constants.CommandSudo) { return []string{constants.CommandSudo} } return []string{} } // IsPackageNotInstalledError checks if an error indicates a package is not installed func IsPackageNotInstalledError(err error) bool { if err == nil { return false } errMsg := strings.ToLower(err.Error()) for _, pattern := range constants.PackageNotFoundPatterns { if strings.Contains(errMsg, pattern) { return true } } return false } // IsPackageNotInstalledInOutput checks if output indicates a package is not installed func IsPackageNotInstalledInOutput(output []string) bool { combinedOutput := strings.ToLower(strings.Join(output, " ")) for _, pattern := range constants.PackageNotFoundPatterns { if strings.Contains(combinedOutput, pattern) { return true } } return false } // IsMPVInstall checks if the given ID is an MPV installation func IsMPVInstall(installID string) bool { return installID == constants.FlatpakMPV || installID == constants.PackageMPV } // GetMethodDisplayName returns the display name for a method ID func GetMethodDisplayName(methodID string) string { if name, ok := constants.MethodIDToName[methodID]; ok { return name } return methodID } // GetAppIDFromMethodID returns the Flatpak/app ID for a method func GetAppIDFromMethodID(methodID string) string { if appID, ok := constants.MethodIDToAppID[methodID]; ok { return appID } return methodID } // EnsureDirectory creates a directory if it doesn't exist func EnsureDirectory(path string, cr *CommandRunner) error { if err := os.MkdirAll(path, constants.DirPermission); err != nil { if cr != nil { cr.outputChan <- fmt.Sprintf("Error: Failed to create directory: %s", path) } return err } if cr != nil { cr.outputChan <- fmt.Sprintf("Created directory: %s", path) } return nil } // WriteFileWithOutput writes a file with logging func WriteFileWithOutput(path string, data []byte, perm os.FileMode, cr *CommandRunner) error { if cr != nil { cr.outputChan <- fmt.Sprintf("Writing to: %s", path) } if err := os.WriteFile(path, data, perm); err != nil { if cr != nil { cr.outputChan <- fmt.Sprintf("Error: Failed to write file: %v", err) } return err } if cr != nil { cr.outputChan <- "Write successful" } return nil } // CopyFileWithOutput copies a file with logging func CopyFileWithOutput(src, dst string, cr *CommandRunner) error { if cr != nil { cr.outputChan <- fmt.Sprintf("Copying: %s -> %s", src, dst) } if err := copyFile(src, dst, constants.FilePermission); err != nil { if cr != nil { cr.outputChan <- fmt.Sprintf("Error: Failed to copy: %v", err) } return err } if cr != nil { cr.outputChan <- "Copied successfully" } return nil } // LogWarning logs a warning to both output channel and log func LogWarning(message string, cr *CommandRunner) { if cr != nil { cr.outputChan <- message } log.Warn(message) } // LogError logs an error to both output channel and log func LogError(err error, cr *CommandRunner) { if cr != nil { cr.outputChan <- fmt.Sprintf("Error: %v", err) } log.Error(err.Error()) } // RemoveAll is a wrapper for os.RemoveAll with logging func RemoveAll(path string) error { if _, err := os.Stat(path); err != nil { return nil } return os.RemoveAll(path) } // InstallUOSCSafely installs uOSC with safe error handling // Logs warning instead of failing if uOSC installation fails func InstallUOSCSafely(installer *Installer, cr *CommandRunner, configDir string) { if err := installer.InstallUOSCWithOutput(cr, configDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to install uOSC: %v", err) } } // InstallMPVConfigSafely installs MPV config with safe error handling // Logs warning instead of failing if config installation fails func InstallMPVConfigSafely(installer *Installer, cr *CommandRunner) { if err := installer.InstallMPVConfigWithOutput(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to install MPV config: %v", err) } } // InstallUISafely installs the specified UI type with safe error handling // Also sets the appropriate osc= setting in mpv.conf // - UITypeUOSC: installs uOSC and sets osc=yes // - UITypeModernZ: installs ModernZ and sets osc=no // - UITypeNone: removes all UI files and osc= setting func InstallUISafely(installer *Installer, cr *CommandRunner, configDir string, uiType string) { // First, remove all existing UI files to prevent conflicts cr.outputChan <- "Removing any existing UI files..." RemoveAllUIFiles(configDir) switch uiType { case constants.UITypeModernZ: if err := installer.InstallModernZWithOutput(cr, configDir); err != nil { cr.outputChan <- fmt.Sprintf("%s: %v", constants.WarnFailedToInstallModernZ, err) return } // ModernZ requires osc=no mpvConfPath := filepath.Join(configDir, constants.MPVConfigFileName) if err := installer.SetOSCConfig(cr, mpvConfPath, "no"); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to set osc=no for ModernZ: %v", err) } case constants.UITypeUOSC: cr.outputChan <- "Installing uOSC (modern OSC UI for MPV)..." if err := installer.InstallUOSCWithOutput(cr, configDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to install uOSC: %v", err) return } // uOSC requires osc=yes mpvConfPath := filepath.Join(configDir, constants.MPVConfigFileName) if err := installer.SetOSCConfig(cr, mpvConfPath, "yes"); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to set osc=yes for uOSC: %v", err) } case constants.UITypeNone: cr.outputChan <- "UI type set to none - using MPV's default interface" // Remove thumbfast.lua since it's only needed for UOSC/ModernZ if err := RemoveThumbfast(configDir); err != nil { cr.outputChan <- fmt.Sprintf("Note: thumbfast.lua not found (already removed)") } else { cr.outputChan <- "Removed thumbfast.lua" } // Set osc=yes to enable MPV's default OSC mpvConfPath := filepath.Join(configDir, constants.MPVConfigFileName) if err := installer.SetOSCDefault(cr, mpvConfPath); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to set osc=yes: %v", err) } default: // Default to ModernZ if err := installer.InstallModernZWithOutput(cr, configDir); err != nil { cr.outputChan <- fmt.Sprintf("%s: %v", constants.WarnFailedToInstallModernZ, err) return } // ModernZ requires osc=no mpvConfPath := filepath.Join(configDir, constants.MPVConfigFileName) if err := installer.SetOSCConfig(cr, mpvConfPath, "no"); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to set osc=no for ModernZ: %v", err) } } } // RemoveUIFiles removes UI files for the specified UI type // This should be called before switching to a different UI type // - UITypeUOSC: removes uosc.lua, uosc.conf, fonts/uosc/ // - UITypeModernZ: removes modernz.lua, modernz.conf, fonts/modernz-icons.ttf // Also cleans up legacy font files (material-design-icons.ttf, fluent-system-icons.ttf) from pre-0.3.1 installs func RemoveUIFiles(configDir string, uiType string) error { scriptsDir := filepath.Join(configDir, constants.ScriptsDir) scriptOptsDir := filepath.Join(configDir, constants.ScriptOptsDir) fontsDir := filepath.Join(configDir, constants.FontsDir) switch uiType { case constants.UITypeUOSC: // Remove uOSC files filesToRemove := []string{ filepath.Join(scriptsDir, "uosc.lua"), filepath.Join(scriptOptsDir, "uosc.conf"), } dirsToRemove := []string{ filepath.Join(scriptsDir, "uosc"), filepath.Join(fontsDir, "uosc"), } for _, f := range filesToRemove { os.Remove(f) // Ignore errors - file may not exist } for _, d := range dirsToRemove { os.RemoveAll(d) // Ignore errors - directory may not exist } case constants.UITypeModernZ: // Remove ModernZ files (current) filesToRemove := []string{ filepath.Join(scriptsDir, "modernz.lua"), filepath.Join(scriptOptsDir, "modernz.conf"), filepath.Join(fontsDir, constants.ModernZFontFile), // Legacy font files from pre-0.3.1 ModernZ releases filepath.Join(fontsDir, "material-design-icons.ttf"), filepath.Join(fontsDir, "fluent-system-icons.ttf"), } for _, f := range filesToRemove { os.Remove(f) // Ignore errors - file may not exist } } return nil } // RemoveAllUIFiles removes all known UI files (both uOSC and ModernZ) // This is useful when switching UI types to ensure a clean slate func RemoveAllUIFiles(configDir string) error { _ = RemoveUIFiles(configDir, constants.UITypeUOSC) _ = RemoveUIFiles(configDir, constants.UITypeModernZ) return nil } // RemoveThumbfast removes the thumbfast.lua script from the scripts directory // This should be called when switching to "No UI" since thumbfast is only needed for UOSC/ModernZ func RemoveThumbfast(configDir string) error { scriptsDir := filepath.Join(configDir, constants.ScriptsDir) thumbfastPath := filepath.Join(scriptsDir, "thumbfast.lua") return os.Remove(thumbfastPath) // Ignore error if file doesn't exist } // WithTempDir creates a temporary directory, runs the provided function, // and ensures cleanup happens on both success and error paths func WithTempDir(prefix string, fn func(string) error) error { tempDir, err := os.MkdirTemp("", prefix) if err != nil { return err } defer os.RemoveAll(tempDir) return fn(tempDir) } // GetMPVConfigDirFromHome returns the MPV config directory path from home directory func GetMPVConfigDirFromHome(homeDir string) string { configPaths := constants.GetConfigPaths(homeDir) return configPaths.MPVConfigDir }