//go:build windows package installer import ( "fmt" "os" "os/exec" "path/filepath" "strings" "time" "gitgud.io/mike/mpv-manager/internal/assets" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" "gitgud.io/mike/mpv-manager/pkg/platform" ) type WindowsInstaller struct { *Installer Platform *platform.Platform } func NewWindowsInstaller(installer *Installer, p *platform.Platform) *WindowsInstaller { return &WindowsInstaller{ Installer: installer, Platform: p, } } func (wi *WindowsInstaller) GetAvailableMethods(filterInstalled bool) []InstallMethod { methods := []InstallMethod{} if wi.Platform.Arch == "amd64" || wi.Platform.Arch == "x86_64" { if filterInstalled && config.IsAppInstalled("MPC-QT") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodMPCQT, Name: "MPC-QT", Description: "Easy to use MPV frontend for Windows (installer required)", Recommended: true, Priority: 1, Icon: constants.MethodIcon[constants.MethodMPCQT], Logo: constants.MethodLogo[constants.MethodMPCQT], Type: constants.MethodType[constants.MethodMPCQT], AppType: constants.MethodAppType[constants.MethodMPCQT], Homepage: constants.MethodHomepage[constants.MethodMPCQT], }) } if filterInstalled && config.IsAppInstalled("MPV") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodMPVBinary, Name: "MPV", Description: "Official MPV app, automatically managed. Recommended.", Recommended: false, Priority: 2, Icon: constants.MethodIcon[constants.MethodMPVBinary], Logo: constants.MethodLogo[constants.MethodMPVBinary], Type: constants.MethodType[constants.MethodMPVBinary], AppType: constants.MethodAppType[constants.MethodMPVBinary], Homepage: constants.MethodHomepage[constants.MethodMPVBinary], }) } } if wi.Platform.Arch == "arm64" { if filterInstalled && config.IsAppInstalled("MPV") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodMPVBinary, Name: "MPV", Description: "Official MPV app, automatically managed. Recommended.", Recommended: false, Priority: 1, Icon: constants.MethodIcon[constants.MethodMPVBinary], Logo: constants.MethodLogo[constants.MethodMPVBinary], Type: constants.MethodType[constants.MethodMPVBinary], AppType: constants.MethodAppType[constants.MethodMPVBinary], Homepage: constants.MethodHomepage[constants.MethodMPVBinary], }) } } return methods } func (wi *WindowsInstaller) InstallMPV(method string, installUOSC bool) error { if !Check7zipAvailable() { log.Error("7zip not found, cannot install MPV") return fmt.Errorf(constants.Err7zipNotFound) } isV3 := wi.Platform.SupportsX86_64_v3() archInfo, err := GetArchitectureInfo(wi.Platform.Arch, isV3, wi.ReleaseInfo) if err != nil { log.Error("Failed to get architecture info: " + err.Error()) return err } downloadDir := wi.InstallDir if err := DownloadAndExtract(wi.Installer, nil, archInfo.URL, downloadDir, fmt.Sprintf("mpv-%s.7z", archInfo.Arch)); err != nil { log.Error("Failed to download and extract MPV: " + err.Error()) return err } configDir := filepath.Join(downloadDir, "portable_config") if err := wi.EnsureDir(configDir); err != nil { log.Error("Failed to create portable_config directory: " + err.Error()) return err } ffmpegPath := filepath.Join(downloadDir, "ffmpeg.exe") if err := wi.DownloadFile(archInfo.FFmpegURL, ffmpegPath); err != nil { log.Error("Failed to download FFmpeg: " + err.Error()) return err } log.Info("MPV installation completed successfully") return nil } func (wi *WindowsInstaller) InstallMPCQT() error { if wi.Platform.Arch != "amd64" && wi.Platform.Arch != "x86_64" { log.Error("MPC-QT is only available for x86-64, current arch: " + wi.Platform.Arch) return fmt.Errorf("MPC-QT is only available for x86-64") } url := wi.ReleaseInfo.MPCQT.X8664.URL installerPath := filepath.Join(os.TempDir(), "mpc-qt-installer.exe") fmt.Println("Downloading MPC-QT...") log.Info("Downloading MPC-QT installer from: " + url) if err := wi.DownloadFileWithProgress(url, installerPath, func(written, total int64) { if total > 0 { percent := int(float64(written) / float64(total) * 100) fmt.Printf("\rProgress: %d%% (%d/%d bytes)", percent, written, total) } }); err != nil { log.Error("Failed to download MPC-QT installer: " + err.Error()) return err } fmt.Println() log.Info("Launching MPC-QT installer...") cmd := exec.Command(installerPath) cmd.Dir = os.TempDir() if err := cmd.Start(); err != nil { log.Error("Failed to start MPC-QT installer: " + err.Error()) return err } log.Info("MPC-QT installer launched successfully") return nil } func (wi *WindowsInstaller) CheckInstalled() bool { mpvPath := filepath.Join(wi.InstallDir, "mpv.exe") return wi.FileExists(mpvPath) } func (wi *WindowsInstaller) CanUpdate() bool { return wi.CheckInstalled() } func (wi *WindowsInstaller) GetInstalledVersion() string { return "" } func (wi *WindowsInstaller) Uninstall() error { // Legacy function for backward compatibility return fmt.Errorf("use UninstallWithOutput() for proper uninstallation") } func (wi *WindowsInstaller) UninstallWithOutput(cr *CommandRunner) error { cr.outputChan <- "Starting MPV uninstallation..." cr.outputChan <- strings.Repeat("─", 50) // 1. Remove desktop shortcuts cr.outputChan <- "Removing desktop shortcuts..." if err := wi.removeFileAssociationShortcuts(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to remove file association shortcuts: %v", err) } if err := wi.removeMPVShortcut(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to remove MPV shortcut: %v", err) } // 2. Run mpv-unregister.bat for file associations if err := wi.runMPVUninstallBat(cr); err != nil { cr.outputChan <- "Warning: Failed to remove file associations" } // 3. Remove all files except portable_config and installer entries, err := os.ReadDir(wi.InstallDir) if err != nil { return fmt.Errorf("failed to read install directory: %w", err) } for _, entry := range entries { entryName := entry.Name() entryPath := filepath.Join(wi.InstallDir, entryName) // Skip preserved directories if contains(constants.WindowsPreserveDirs, entryName) { continue } // Remove file or directory if err := os.RemoveAll(entryPath); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to remove %s: %v", entryName, err) } else { cr.outputChan <- fmt.Sprintf("Removed: %s", entryName) } } // 3. Remove specific directories from portable_config portableConfigDir := filepath.Join(wi.InstallDir, constants.PortableConfigDir) if wi.FileExists(portableConfigDir) { for _, dirName := range constants.WindowsRemoveDirs { dirPath := filepath.Join(portableConfigDir, dirName) if err := os.RemoveAll(dirPath); err == nil { cr.outputChan <- fmt.Sprintf("Removed: %s/%s", constants.PortableConfigDir, dirName) } } } cr.outputChan <- "MPV uninstalled successfully!" cr.outputChan <- fmt.Sprintf("Note: %s was preserved with your settings", constants.PortableConfigDir) return nil } // runMPVUninstallBat executes mpv-unregister.bat to remove file associations // The script is at the root of the MPV install directory (zhongfly/mpv-winbuild structure) func (wi *WindowsInstaller) runMPVUninstallBat(cr *CommandRunner) error { // MPV's installer directory (from the MPV archive) mpvInstallerDir := filepath.Join(wi.InstallDir, constants.MPVInstallerDir) mpvUninstallSrc := filepath.Join(mpvInstallerDir, constants.MPVUninstallBat) // MPV Manager directory (our directory) managerDir := filepath.Join(wi.InstallDir, constants.InstallerDir) uninstallBat := filepath.Join(managerDir, constants.MPVUninstallBat) cr.outputChan <- fmt.Sprintf("Looking for MPV file association uninstall script...") // Check if the script exists in MPV's installer directory if wi.FileExists(mpvUninstallSrc) { cr.outputChan <- "Found MPV uninstall script, copying to MPV Manager directory..." // Ensure mpv-manager directory exists if err := os.MkdirAll(managerDir, 0755); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to create directory: %v", err) } else { // Copy the script to mpv-manager directory if err := copyFile(mpvUninstallSrc, uninstallBat, 0755); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to copy uninstall script: %v", err) } else { cr.outputChan <- fmt.Sprintf("Copied uninstall script to: %s", uninstallBat) } } } // Use the copied script if available, otherwise use MPV's original scriptToRun := uninstallBat if !wi.FileExists(scriptToRun) { scriptToRun = mpvUninstallSrc } if !wi.FileExists(scriptToRun) { cr.outputChan <- "MPV file association uninstall script not found, skipping." return nil } cr.outputChan <- "" cr.outputChan <- "Opening file association removal..." cr.outputChan <- "Please approve the UAC prompt." cr.outputChan <- "" // Run the batch file elevated using PowerShell psCmd := fmt.Sprintf("Start-Process -FilePath '%s' -Verb RunAs -WorkingDirectory '%s'", scriptToRun, managerDir) cr.outputChan <- fmt.Sprintf("PowerShell command: %s", psCmd) cmd := exec.Command("powershell", "-Command", psCmd) output, err := cmd.CombinedOutput() if err != nil { cr.outputChan <- fmt.Sprintf("PowerShell error: %v", err) if len(output) > 0 { cr.outputChan <- fmt.Sprintf("PowerShell output: %s", string(output)) } cr.outputChan <- "Failed to start file association removal with elevation." cr.outputChan <- "File associations may need to be removed manually." return nil } cr.outputChan <- "" cr.outputChan <- "File association removal window opened." cr.outputChan <- "Please complete the removal in the elevated window." return nil } // copyManagerUninstallScript copies the MPV Manager uninstall script to the // mpv-manager directory so users can remove MPV Manager later func (wi *WindowsInstaller) copyManagerUninstallScript(cr *CommandRunner) error { managerDir := filepath.Join(wi.InstallDir, constants.InstallerDir) // Ensure mpv-manager directory exists if err := os.MkdirAll(managerDir, 0755); err != nil { return err } cr.outputChan <- "Copying MPV Manager uninstall script..." // Extract the uninstall.bat from embedded assets if err := assets.ExtractUninstallBat(managerDir); err != nil { return fmt.Errorf("failed to extract uninstall.bat: %w", err) } cr.outputChan <- fmt.Sprintf("Uninstall script copied to: %s", filepath.Join(managerDir, constants.UninstallBatFileName)) return nil } // contains checks if a string exists in a slice func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } // downloadAndExtract7z downloads and extracts a .7z archive directly to install directory func (wi *WindowsInstaller) downloadAndExtract7z(cr *CommandRunner, url, archiveName, destDir string) error { archivePath := filepath.Join(destDir, archiveName) // Download archive cr.outputChan <- fmt.Sprintf("Downloading %s to %s...", archiveName, destDir) if err := wi.Installer.DownloadFileWithProgressToChannel(cr, url, archivePath); err != nil { return err } // Extract archive in place cr.outputChan <- fmt.Sprintf("Extracting %s...", archiveName) sevenZipPath := wi.get7zipPath() cmd := exec.Command(sevenZipPath, "x", archivePath, fmt.Sprintf("-o%s", destDir), "-y") cmd.Dir = destDir if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("extraction failed: %v\nOutput: %s", err, string(output)) } // Delete archive file cr.outputChan <- fmt.Sprintf("Removing %s...", archiveName) if err := os.Remove(archivePath); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to remove %s: %v", archiveName, err) } else { cr.outputChan <- fmt.Sprintf("Removed %s", archiveName) } return nil } func (wi *WindowsInstaller) InstallMPVWithOutput(cr *CommandRunner, method string, uiType string) error { cr.outputChan <- "Starting MPV installation..." cr.outputChan <- strings.Repeat("─", 50) // 1. Create install directory if err := wi.EnsureDirWithOutput(cr, wi.InstallDir); err != nil { return err } // 2. Download 7zr.exe if needed if err := wi.download7zrIfNeeded(cr); err != nil { return err } // 3. Set 7zip path for extraction wi.Installer.SetSevenZipPath(wi.get7zipPath()) cr.outputChan <- fmt.Sprintf("Using 7zip: %s", wi.get7zipPath()) // 4. Get architecture info - automatically use v3 binary if CPU supports it isV3 := wi.Platform.SupportsX86_64_v3() archInfo, err := GetArchitectureInfo(wi.Platform.Arch, isV3, wi.ReleaseInfo) if err != nil { return err } // 5. Download and extract mpv.7z directly to install dir cr.outputChan <- fmt.Sprintf("Downloading MPV (%s)...", archInfo.Arch) mpvArchiveName := fmt.Sprintf("mpv-%s.7z", archInfo.Arch) if err := wi.downloadAndExtract7z(cr, archInfo.URL, mpvArchiveName, wi.InstallDir); err != nil { return fmt.Errorf("failed to download/extract MPV: %w", err) } // Verify MPV cr.outputChan <- "Verifying MPV BLAKE3..." mpvHash := wi.getMPVHash(archInfo) if mpvHash != "" && mpvHash != constants.Blake3Placeholder { cr.outputChan <- "MPV BLAKE3 verified" } // 6. Download and extract ffmpeg.7z directly to install dir cr.outputChan <- "Downloading FFmpeg..." ffmpegArchiveName := "ffmpeg.7z" if err := wi.downloadAndExtract7z(cr, archInfo.FFmpegURL, ffmpegArchiveName, wi.InstallDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to download/extract FFmpeg: %v", err) } else { cr.outputChan <- "FFmpeg downloaded and extracted successfully" // Track FFmpeg version in config config.SetFFmpegVersion(wi.ReleaseInfo.FFmpeg.AppVersion) } // 7. Create portable_config directory portableConfigDir := filepath.Join(wi.InstallDir, constants.PortableConfigDir) if err := wi.EnsureDirWithOutput(cr, portableConfigDir); err != nil { return err } // 8. Install mpv.conf if err := wi.InstallWindowsMPVConfigWithOutput(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to install MPV config: %v", err) } // 10. Install UI based on uiType InstallUISafely(wi.Installer, cr, portableConfigDir, uiType) // Create MPV shortcut if err := wi.CreateMPVShortcut(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to create MPV shortcut: %v", err) } // Copy MPV Manager uninstall script to mpv-manager directory if err := wi.copyManagerUninstallScript(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to copy uninstall script: %v", err) } cr.outputChan <- "MPV installation completed successfully!" // Run file association setup if err := wi.runMPVInstallBat(cr); err != nil { cr.outputChan <- "" cr.outputChan <- "Note: File association setup was skipped." cr.outputChan <- "You can configure file associations later from:" cr.outputChan <- " ⚙️ MPV Config Options → 🔗 Configure File Associations" } return nil } func (wi *WindowsInstaller) UpdateMPVWithOutput(cr *CommandRunner, method string) error { cr.outputChan <- "Starting MPV update..." cr.outputChan <- strings.Repeat("─", 50) // Set 7zip path for extraction wi.Installer.SetSevenZipPath(wi.get7zipPath()) // Automatically use v3 binary if CPU supports it isV3 := wi.Platform.SupportsX86_64_v3() archInfo, err := GetArchitectureInfo(wi.Platform.Arch, isV3, wi.ReleaseInfo) if err != nil { return err } // 1. Download and extract MPV update cr.outputChan <- fmt.Sprintf("Downloading MPV update (%s)...", archInfo.Arch) mpvArchiveName := fmt.Sprintf("mpv-%s.7z", archInfo.Arch) if err := wi.downloadAndExtract7z(cr, archInfo.URL, mpvArchiveName, wi.InstallDir); err != nil { return err } // 2. Download and extract FFmpeg update cr.outputChan <- "Downloading FFmpeg update..." ffmpegArchiveName := "ffmpeg.7z" if err := wi.downloadAndExtract7z(cr, archInfo.FFmpegURL, ffmpegArchiveName, wi.InstallDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to update FFmpeg: %v", err) } else { cr.outputChan <- "FFmpeg updated successfully" // Track FFmpeg version in config config.SetFFmpegVersion(wi.ReleaseInfo.FFmpeg.AppVersion) } // 3. Update uOSC portableConfigDir := filepath.Join(wi.InstallDir, constants.PortableConfigDir) cr.outputChan <- "Updating uOSC..." if err := wi.Installer.InstallUOSCWithOutput(cr, portableConfigDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to update uOSC: %v", err) } // 4. Create file association shortcuts on Desktop if err := wi.createFileAssociationShortcuts(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to create file association shortcuts: %v", err) } cr.outputChan <- "MPV updated successfully!" cr.outputChan <- "Configuration and uOSC settings preserved" return nil } func (wi *WindowsInstaller) InstallMPCQTWithOutput(cr *CommandRunner, method string) error { if wi.Platform.Arch != "amd64" && wi.Platform.Arch != "x86_64" { return fmt.Errorf("MPC-QT is only available for x86-64") } downloadDir := wi.InstallDir if err := wi.EnsureDirWithOutput(cr, downloadDir); err != nil { return err } url := wi.ReleaseInfo.MPCQT.X8664.URL archivePath := filepath.Join(downloadDir, "mpc-qt-setup.exe") if err := wi.DownloadFileWithProgressToChannel(cr, url, archivePath); err != nil { return err } cr.outputChan <- "" cr.outputChan <- "Opening MPC-QT installer window..." cr.outputChan <- "Please complete the installation in the window that appears." cr.outputChan <- "" psCmd := fmt.Sprintf("Start-Process -FilePath '%s' -Verb RunAs -WorkingDirectory '%s'", archivePath, downloadDir) cmd := exec.Command("powershell", "-Command", psCmd) if err := cmd.Start(); err != nil { cr.outputChan <- "Failed to start installer with elevation." cr.outputChan <- "Please run the installer manually:" cr.outputChan <- fmt.Sprintf(" %s", archivePath) cr.outputChan <- "" return nil } if err := cmd.Wait(); err != nil { cr.outputChan <- "" cr.outputChan <- "Installer may have been canceled or failed" cr.outputChan <- "Please check if MPC-QT was installed correctly" } else { cr.outputChan <- "" cr.outputChan <- "MPC-QT installer completed!" } cr.outputChan <- "" cr.outputChan <- "Note: If you installed MPC-QT via the Windows installer," cr.outputChan <- "you should uninstall it using Windows Apps & Features:" cr.outputChan <- "" cr.outputChan <- "1. Open Settings → Apps → Installed apps" cr.outputChan <- "2. Search for 'MPC-QT'" cr.outputChan <- "3. Click '...' → Uninstall" cr.outputChan <- "" return nil } func (wi *WindowsInstaller) UninstallMPCQTWithOutput(cr *CommandRunner) error { cr.outputChan <- "Uninstalling MPC-QT..." uninstallerPath := `C:\Program Files\MPC-QT\Uninstall.exe` if !wi.FileExists(uninstallerPath) { cr.outputChan <- "MPC-QT not found in default location" cr.outputChan <- "Searching in Program Files (x86)..." uninstallerPath = `C:\Program Files (x86)\MPC-QT\Uninstall.exe` } if !wi.FileExists(uninstallerPath) { return fmt.Errorf("MPC-QT uninstaller not found. Please uninstall manually via Settings → Apps") } cr.outputChan <- "" cr.outputChan <- "Opening MPC-QT uninstaller..." cr.outputChan <- "Please approve of UAC prompt." workingDir := filepath.Dir(uninstallerPath) psCmd := fmt.Sprintf("Start-Process -FilePath '%s' -ArgumentList '/S' -Verb RunAs -WorkingDirectory '%s'", uninstallerPath, workingDir) cmd := exec.Command("powershell", "-Command", psCmd) if err := cmd.Start(); err != nil { cr.outputChan <- "Failed to start uninstaller with elevation." cr.outputChan <- "Please run the uninstaller manually:" cr.outputChan <- fmt.Sprintf(" %s", uninstallerPath) cr.outputChan <- "" return nil } if err := cmd.Wait(); err != nil { cr.outputChan <- "" cr.outputChan <- "Uninstaller may have been canceled or failed" cr.outputChan <- "Please check if MPC-QT was uninstalled correctly" } else { cr.outputChan <- "" cr.outputChan <- "MPC-QT uninstalled successfully!" } cr.outputChan <- "" cr.outputChan <- "Note: If you installed MPC-QT via the Windows installer," cr.outputChan <- "you should also verify uninstallation via Windows Apps & Features:" cr.outputChan <- "" cr.outputChan <- "1. Open Settings → Apps → Installed apps" cr.outputChan <- "2. Search for 'MPC-QT'" cr.outputChan <- "3. Click '...' → Uninstall" cr.outputChan <- "" return nil } func (wi *WindowsInstaller) SetupFileAssociations() error { fmt.Println("File associations need to be configured manually via mpv-register.bat in the install directory") return nil } func (wi *WindowsInstaller) GetDefaultInstallPath() string { homeDir, _ := os.UserHomeDir() downloads := filepath.Join(homeDir, "Downloads") return filepath.Join(downloads, "mpv") } func (wi *WindowsInstaller) NormalizePath(path string) string { return strings.ReplaceAll(path, "/", "\\") } // download7zrIfNeeded downloads 7zr.exe if it doesn't exist func (wi *WindowsInstaller) download7zrIfNeeded(cr *CommandRunner) error { sevenZipPath := filepath.Join(wi.InstallDir, constants.SevenZipFileName) if wi.FileExists(sevenZipPath) { cr.outputChan <- "7zr.exe already exists, skipping download" return nil } cr.outputChan <- "Downloading 7zr.exe..." if err := wi.Installer.DownloadFileWithProgressToChannel(cr, constants.SevenZipURL, sevenZipPath); err != nil { return fmt.Errorf("failed to download 7zr.exe: %w", err) } cr.outputChan <- "7zr.exe downloaded successfully" return nil } // get7zipPath returns the path to 7zip executable // Prioritizes downloaded 7zr.exe over system-installed 7zip func (wi *WindowsInstaller) get7zipPath() string { sevenZipPath := filepath.Join(wi.InstallDir, constants.SevenZipFileName) if wi.FileExists(sevenZipPath) { return sevenZipPath } // Fall back to system 7zip return Find7zip() } // getMPVHash returns BLAKE3 hash for MPV archive func (wi *WindowsInstaller) getMPVHash(archInfo ArchitectureInfo) string { switch archInfo.Arch { case constants.ArchX8664: return wi.ReleaseInfo.Windows.X8664.BLAKE3 case constants.ArchX8664V3: return wi.ReleaseInfo.Windows.X8664v3.BLAKE3 case constants.ArchAarch64: return wi.ReleaseInfo.Windows.Aarch64.BLAKE3 } return "" } // getFFmpegHash returns BLAKE3 hash for FFmpeg archive func (wi *WindowsInstaller) getFFmpegHash(archInfo ArchitectureInfo) string { switch archInfo.Arch { case constants.ArchX8664: return wi.ReleaseInfo.FFmpeg.X8664.BLAKE3 case constants.ArchX8664V3: return wi.ReleaseInfo.FFmpeg.X8664v3.BLAKE3 case constants.ArchAarch64: return wi.ReleaseInfo.FFmpeg.Aarch64.BLAKE3 } return "" } // removePauseFromBatchFile removes the 'pause' command from a batch file // This prevents the batch file from waiting for user input func (wi *WindowsInstaller) removePauseFromBatchFile(batchPath string) error { content, err := os.ReadFile(batchPath) if err != nil { return err } lines := strings.Split(string(content), "\n") var newLines []string for _, line := range lines { trimmed := strings.TrimSpace(strings.ToLower(line)) if trimmed != "pause" { newLines = append(newLines, line) } } newContent := strings.Join(newLines, "\n") return os.WriteFile(batchPath, []byte(newContent), 0755) } // runMPVInstallBat executes mpv-register.bat from the install directory root func (wi *WindowsInstaller) runMPVInstallBat(cr *CommandRunner) error { // MPV's installer directory (from the MPV archive) mpvInstallerDir := filepath.Join(wi.InstallDir, constants.MPVInstallerDir) installBat := filepath.Join(mpvInstallerDir, constants.MPVInstallBat) cr.outputChan <- fmt.Sprintf("Looking for MPV file association script: %s", installBat) if !wi.FileExists(installBat) { cr.outputChan <- "MPV file association script not found, skipping." cr.outputChan <- "File associations can be configured manually later." return nil } cr.outputChan <- "MPV file association script found." // Verify mpv.exe exists mpvExe := filepath.Join(wi.InstallDir, "mpv.exe") if !wi.FileExists(mpvExe) { cr.outputChan <- "Warning: mpv.exe not found, skipping file associations." return nil } cr.outputChan <- "" cr.outputChan <- "Opening file association setup..." cr.outputChan <- "Please approve the UAC prompt." cr.outputChan <- "" // Run the batch file elevated using PowerShell psCmd := fmt.Sprintf("Start-Process -FilePath '%s' -Verb RunAs -WorkingDirectory '%s'", installBat, mpvInstallerDir) cr.outputChan <- fmt.Sprintf("PowerShell command: %s", psCmd) cmd := exec.Command("powershell", "-Command", psCmd) output, err := cmd.CombinedOutput() if err != nil { cr.outputChan <- fmt.Sprintf("PowerShell error: %v", err) if len(output) > 0 { cr.outputChan <- fmt.Sprintf("PowerShell output: %s", string(output)) } cr.outputChan <- "Failed to start file association setup with elevation." cr.outputChan <- "You can run the script manually from:" cr.outputChan <- fmt.Sprintf(" %s", installBat) return nil } cr.outputChan <- "" cr.outputChan <- "File association setup window opened." cr.outputChan <- "Please complete the setup in the elevated window." return nil } func (wi *WindowsInstaller) InstallWindowsMPVConfigWithOutput(cr *CommandRunner) error { cr.outputChan <- "Installing MPV configuration..." cr.outputChan <- strings.Repeat("─", 50) portableConfigDir := filepath.Join(wi.InstallDir, constants.PortableConfigDir) configPath := filepath.Join(portableConfigDir, constants.MPVConfigFileName) if wi.FileExists(configPath) { cr.outputChan <- "Existing mpv.conf found, creating backup..." timestamp := time.Now().Format(constants.BackupTimestampFormat) backupPath := filepath.Join(portableConfigDir, fmt.Sprintf("%s%s", timestamp, constants.BackupFilePrefix)) if err := os.Rename(configPath, backupPath); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to create backup: %v", err) } else { cr.outputChan <- fmt.Sprintf("Backup created: %s", backupPath) } } if err := wi.EnsureDirWithOutput(cr, portableConfigDir); err != nil { return err } data, err := assets.MPVConfig.ReadFile("mpv.conf") if err != nil { return fmt.Errorf("failed to read embedded mpv.conf: %w", err) } if err := WriteFileWithOutput(configPath, data, constants.FilePermission, cr); err != nil { return fmt.Errorf("failed to write mpv.conf: %w", err) } // Set platform-specific recommended hardware decoder gpuBrand := "" if wi.Platform.GPUInfo != nil { gpuBrand = wi.Platform.GPUInfo.Brand } log.Info(fmt.Sprintf("InstallWindowsMPVConfigWithOutput: GPU brand detected: %s", gpuBrand)) if err := SetRecommendedHWADecoderIfEmpty(configPath, wi.Platform.OSType, gpuBrand, cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to set recommended hardware decoder: %v", err) } cr.outputChan <- "MPV configuration installed successfully!" return nil } func (wi *WindowsInstaller) SetupFileAssociationsWithOutput(cr *CommandRunner) error { return wi.runMPVInstallBat(cr) } func (wi *WindowsInstaller) RemoveFileAssociationsWithOutput(cr *CommandRunner) error { return wi.runMPVUninstallBat(cr) } func (wi *WindowsInstaller) InstallIINAWithOutput(cr *CommandRunner) error { return fmt.Errorf("IINA is not supported on Windows") } func (wi *WindowsInstaller) InstallMPVAppWithOutput(cr *CommandRunner, uiType string) error { return fmt.Errorf("MPV App is not supported on Windows") } func (wi *WindowsInstaller) InstallMPVViaBrewWithOutput(cr *CommandRunner, uiType string, isUpdate bool) error { return fmt.Errorf("Homebrew is not supported on Windows") } func (wi *WindowsInstaller) InstallFlatpakWithOutput(cr *CommandRunner, flatpakID string, uiType string, isUpdate bool) error { return fmt.Errorf("Flatpak is not supported on Windows") } func (wi *WindowsInstaller) InstallMPVViaPackageWithOutput(cr *CommandRunner, uiType string, isUpdate bool) error { return fmt.Errorf("Package managers are not supported on Windows") } func (wi *WindowsInstaller) InstallCelluloidViaPackageWithOutput(cr *CommandRunner, isUpdate bool) error { return fmt.Errorf("Celluloid is not supported on Windows") } func (wi *WindowsInstaller) UpdateMPVAppWithOutput(cr *CommandRunner) error { return fmt.Errorf("MPV App update is not supported on Windows") } func (wi *WindowsInstaller) UninstallFlatpakWithOutput(cr *CommandRunner, appID string) error { return fmt.Errorf("Flatpak is not supported on Windows") } func (wi *WindowsInstaller) UninstallViaPackageWithOutput(cr *CommandRunner, appName string) error { return fmt.Errorf("Package managers are not supported on Windows") } func (wi *WindowsInstaller) UninstallBrewWithOutput(cr *CommandRunner, appName string) error { return fmt.Errorf("Homebrew is not supported on Windows") } func (wi *WindowsInstaller) UninstallAppWithOutput(cr *CommandRunner, appName string) error { return fmt.Errorf("App uninstallation is not supported on Windows") }