//go:build windows package installer import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "gitgud.io/mike/mpv-manager/internal/assets" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" ) func (wi *WindowsInstaller) ensureShortcutsAssets(cr *CommandRunner) error { installerDir := filepath.Join(wi.InstallDir, constants.InstallerDir) // MPV's scripts are at the root of the install directory (zhongfly/mpv-winbuild) // constants.MPVInstallerDir is "" for zhongfly builds mpvScriptsDir := filepath.Join(wi.InstallDir, constants.MPVInstallerDir) // Ensure our mpv-manager directory exists if err := os.MkdirAll(installerDir, 0755); err != nil { return fmt.Errorf("failed to create mpv-manager directory: %w", err) } // Extract VBS script for shortcut creation vbsPath := filepath.Join(installerDir, "create-shortcut.vbs") if !wi.FileExists(vbsPath) { cr.outputChan <- "Extracting shortcut creation script..." if err := assets.ExtractCreateShortcutVBS(installerDir); err != nil { return fmt.Errorf("failed to extract VBS script: %w", err) } } // Copy MPV's register/unregister scripts from the install root to our directory // This allows the shortcuts to work and persists the scripts after MPV is updated mpvInstallBat := filepath.Join(mpvScriptsDir, constants.MPVInstallBat) ourInstallBat := filepath.Join(installerDir, constants.MPVInstallBat) if wi.FileExists(mpvInstallBat) && !wi.FileExists(ourInstallBat) { cr.outputChan <- "Copying file association scripts..." if err := copyFile(mpvInstallBat, ourInstallBat, 0755); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to copy %s: %v", constants.MPVInstallBat, err) } } mpvUninstallBat := filepath.Join(mpvScriptsDir, constants.MPVUninstallBat) ourUninstallBat := filepath.Join(installerDir, constants.MPVUninstallBat) if wi.FileExists(mpvUninstallBat) && !wi.FileExists(ourUninstallBat) { if err := copyFile(mpvUninstallBat, ourUninstallBat, 0755); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to copy %s: %v", constants.MPVUninstallBat, err) } } // NOTE: zhongfly/mpv-winbuild does not include .ico files. // We use mpv.exe as the icon source for shortcuts (it has an embedded icon). // Extract MPV Manager uninstall script managerUninstallBat := filepath.Join(installerDir, constants.UninstallBatFileName) if !wi.FileExists(managerUninstallBat) { if err := assets.ExtractUninstallBat(installerDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to extract %s: %v", constants.UninstallBatFileName, err) // Don't fail the entire operation for this } } return nil } func (wi *WindowsInstaller) createShortcut(cr *CommandRunner, shortcutPath, targetPath, workingDir, description, iconPath string) error { installerDir := filepath.Join(wi.InstallDir, constants.InstallerDir) vbsPath := filepath.Join(installerDir, "create-shortcut.vbs") cmd := exec.Command("wscript.exe", vbsPath, shortcutPath, targetPath, workingDir, description, iconPath) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to create shortcut: %w, output: %s", err, string(output)) } return nil } func (wi *WindowsInstaller) createDesktopShortcut(cr *CommandRunner, shortcutName, targetPath, workingDir, description, iconPath string) error { homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %w", err) } desktopPath := filepath.Join(homeDir, "Desktop") shortcutPath := filepath.Join(desktopPath, shortcutName) cr.outputChan <- fmt.Sprintf("Creating Desktop shortcut: %s", shortcutName) if err := wi.createShortcut(cr, shortcutPath, targetPath, workingDir, description, iconPath); err != nil { return err } return nil } func (wi *WindowsInstaller) createStartMenuShortcut(cr *CommandRunner, shortcutName, targetPath, workingDir, description, iconPath string) error { appData := os.Getenv("APPDATA") if appData == "" { return fmt.Errorf("could not get APPDATA path") } startMenuPath := filepath.Join(appData, "Microsoft", "Windows", "Start Menu", "Programs") shortcutPath := filepath.Join(startMenuPath, shortcutName) cr.outputChan <- fmt.Sprintf("Creating Start Menu shortcut: %s", shortcutName) if err := wi.createShortcut(cr, shortcutPath, targetPath, workingDir, description, iconPath); err != nil { return err } return nil } func (wi *WindowsInstaller) createFileAssociationShortcuts(cr *CommandRunner) error { installerDir := filepath.Join(wi.InstallDir, constants.InstallerDir) if err := wi.ensureShortcutsAssets(cr); err != nil { return err } installBat := filepath.Join(installerDir, constants.MPVInstallBat) // Use mpv.exe as icon source (zhongfly/mpv-winbuild doesn't include .ico files) mpvExePath := filepath.Join(wi.InstallDir, "mpv.exe") installIcon := mpvExePath if !wi.FileExists(mpvExePath) { installIcon = "" // Fallback to no icon } // Only create shortcut if the batch file exists if wi.FileExists(installBat) { if err := wi.createDesktopShortcut( cr, constants.ShortcutInstallName, installBat, installerDir, "Set MPV as Default Media Player (Run as Administrator)", installIcon, ); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to create install shortcut: %v", err) } } uninstallBat := filepath.Join(installerDir, constants.MPVUninstallBat) // Only create shortcut if the batch file exists if wi.FileExists(uninstallBat) { if err := wi.createDesktopShortcut( cr, constants.ShortcutUninstallName, uninstallBat, installerDir, "Remove MPV from File Defaults (Run as Administrator)", installIcon, ); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to create uninstall shortcut: %v", err) } } return nil } func (wi *WindowsInstaller) removeFileAssociationShortcuts(cr *CommandRunner) error { homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %w", err) } desktopPath := filepath.Join(homeDir, "Desktop") installShortcut := filepath.Join(desktopPath, constants.ShortcutInstallName) if wi.FileExists(installShortcut) { cr.outputChan <- "Removing file association shortcut..." if err := os.Remove(installShortcut); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to remove %s: %v", constants.ShortcutInstallName, err) } else { cr.outputChan <- "Removed: " + constants.ShortcutInstallName } } uninstallShortcut := filepath.Join(desktopPath, constants.ShortcutUninstallName) if wi.FileExists(uninstallShortcut) { if err := os.Remove(uninstallShortcut); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to remove %s: %v", constants.ShortcutUninstallName, err) } else { cr.outputChan <- "Removed: " + constants.ShortcutUninstallName } } return nil } func (wi *WindowsInstaller) CreateMPVShortcut(cr *CommandRunner) error { mpvPath := filepath.Join(wi.InstallDir, "mpv.exe") if !wi.FileExists(mpvPath) { cr.outputChan <- "mpv.exe not found, skipping MPV shortcut" return nil } // Ensure VBS script and other assets are extracted if err := wi.ensureShortcutsAssets(cr); err != nil { return fmt.Errorf("failed to prepare shortcut assets: %w", err) } // Use MPV's built-in icon directly (exe provides its own icon) iconPath := mpvPath // Create Desktop shortcut if err := wi.createDesktopShortcut( cr, constants.ShortcutMPVName, mpvPath, wi.InstallDir, "MPV Media Player", iconPath, ); err != nil { return err } // Create Start Menu shortcut if err := wi.createStartMenuShortcut( cr, constants.ShortcutMPVName, mpvPath, wi.InstallDir, "MPV Media Player", iconPath, ); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to create Start Menu shortcut: %v", err) // Don't return error - desktop shortcut was created successfully } return nil } func (wi *WindowsInstaller) createInstallerShortcut(cr *CommandRunner) error { execPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to get executable path: %w", err) } // Ensure VBS script and other assets are extracted if err := wi.ensureShortcutsAssets(cr); err != nil { return fmt.Errorf("failed to prepare shortcut assets: %w", err) } installerDir := filepath.Join(wi.InstallDir, constants.InstallerDir) if err := os.MkdirAll(installerDir, 0755); err != nil { return fmt.Errorf("failed to create installer dir: %w", err) } iconPath := filepath.Join(installerDir, constants.InstallerIco) if !wi.FileExists(iconPath) { if err := assets.ExtractInstallMPVIco(installerDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to extract icon: %v", err) iconPath = "" } } execDir := filepath.Dir(execPath) if err := wi.createDesktopShortcut( cr, constants.ShortcutInstallerName, execPath, execDir, "MPV.Rocks Installer - Manage MPV Player", iconPath, ); err != nil { return err } return nil } func (wi *WindowsInstaller) createInstallerShortcutToBinary(cr *CommandRunner, binaryPath string) error { // Ensure VBS script and other assets are extracted if err := wi.ensureShortcutsAssets(cr); err != nil { return fmt.Errorf("failed to prepare shortcut assets: %w", err) } installerDir := filepath.Join(wi.InstallDir, constants.InstallerDir) if err := os.MkdirAll(installerDir, 0755); err != nil { return fmt.Errorf("failed to create installer dir: %w", err) } iconPath := filepath.Join(installerDir, constants.InstallerIco) if !wi.FileExists(iconPath) { if err := assets.ExtractInstallMPVIco(installerDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to extract icon: %v", err) iconPath = "" } } execDir := filepath.Dir(binaryPath) if err := wi.createDesktopShortcut( cr, constants.ShortcutInstallerName, binaryPath, execDir, "MPV.Rocks Installer - Manage MPV Player", iconPath, ); err != nil { return err } return nil } func (wi *WindowsInstaller) CreateInstallerShortcutWithOutput(cr *CommandRunner) error { cr.outputChan <- "Creating MPV Manager shortcut on Desktop..." if err := wi.createInstallerShortcut(cr); err != nil { cr.outputChan <- fmt.Sprintf("Error creating shortcut: %v", err) return err } cr.outputChan <- "" cr.outputChan <- "✓ MPV Manager shortcut created successfully!" cr.outputChan <- "" cr.outputChan <- "Desktop shortcut created:" cr.outputChan <- " • \"MPV Manager\"" cr.outputChan <- "" cr.outputChan <- "You can now quickly launch the installer from your Desktop." return nil } func (wi *WindowsInstaller) CreateInstallerShortcutToBinaryWithOutput(cr *CommandRunner, binaryPath string) error { cr.outputChan <- "Creating MPV Manager shortcut on Desktop..." if err := wi.createInstallerShortcutToBinary(cr, binaryPath); err != nil { cr.outputChan <- fmt.Sprintf("Error creating shortcut: %v", err) return err } cr.outputChan <- "" cr.outputChan <- "✓ MPV Manager shortcut created successfully!" cr.outputChan <- "" cr.outputChan <- "Desktop shortcut created:" cr.outputChan <- " • \"MPV Manager\"" cr.outputChan <- "" cr.outputChan <- "You can now quickly launch the installer from your Desktop." return nil } func CreateWebUIShortcutWithOutput(cr *CommandRunner) error { execPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to get executable path: %w", err) } cr.outputChan <- "Setting up MPV Manager..." homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %w", err) } desktopPath := filepath.Join(homeDir, "Desktop") // Get the install directory from config (respects %APPDATA%\mpv for fresh installs) installBaseDir := config.GetInstallPath() installDir := filepath.Join(installBaseDir, constants.InstallerDir) if err := os.MkdirAll(installDir, 0755); err != nil { return fmt.Errorf("failed to create installer dir: %w", err) } // Copy executable to install directory if not already there or if different binaryPath := filepath.Join(installDir, "mpv-manager.exe") shouldCopy := false if _, err := os.Stat(binaryPath); os.IsNotExist(err) { shouldCopy = true cr.outputChan <- "Copying MPV Manager to user directory..." } else { // Check if the binary is different (e.g., newer version) currentExe, _ := os.ReadFile(execPath) existingBinary, _ := os.ReadFile(binaryPath) if !bytes.Equal(currentExe, existingBinary) { shouldCopy = true cr.outputChan <- "Updating MPV Manager binary..." } } if shouldCopy { input, err := os.ReadFile(execPath) if err != nil { return fmt.Errorf("failed to read executable: %w", err) } if err := os.WriteFile(binaryPath, input, 0755); err != nil { return fmt.Errorf("failed to copy executable: %w", err) } cr.outputChan <- "✓ MPV Manager binary copied" } // Extract icon iconPath := filepath.Join(installDir, constants.InstallerIco) if _, err := os.Stat(iconPath); os.IsNotExist(err) { if err := assets.ExtractInstallMPVIco(installDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to extract icon: %v", err) iconPath = "" } } // Extract uninstall script uninstallBat := filepath.Join(installDir, constants.UninstallBatFileName) if _, err := os.Stat(uninstallBat); os.IsNotExist(err) { if err := assets.ExtractUninstallBat(installDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to extract uninstall script: %v", err) } } // Create shortcut pointing to the copied binary shortcutPath := filepath.Join(desktopPath, constants.ShortcutInstallerName) targetPath := binaryPath arguments := "-m web" workingDir := installDir vbsScript := ` Set WshShell = CreateObject("WScript.Shell") Set Shortcut = WshShell.CreateShortcut("` + shortcutPath + `") Shortcut.TargetPath = "` + targetPath + `" Shortcut.Arguments = "` + arguments + `" Shortcut.WorkingDirectory = "` + workingDir + `" Shortcut.Description = "MPV.Rocks Installer - Web UI Mode" If "` + iconPath + `" <> "" Then Shortcut.IconLocation = "` + iconPath + `" End If Shortcut.Save ` vbsPath := filepath.Join(installDir, "create-webui-shortcut.vbs") if err := os.WriteFile(vbsPath, []byte(vbsScript), 0644); err != nil { return fmt.Errorf("failed to write VBS script: %w", err) } cmd := exec.Command("cscript", "//NoLogo", vbsPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("failed to create shortcut: %w", err) } cr.outputChan <- "✓ Desktop shortcut created: " + constants.ShortcutInstallerName return nil } func (wi *WindowsInstaller) CreateWebUIShortcutWithOutput(cr *CommandRunner) error { return CreateWebUIShortcutWithOutput(cr) } func (wi *WindowsInstaller) removeMPVShortcut(cr *CommandRunner) error { homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %w", err) } // Remove Desktop shortcut desktopPath := filepath.Join(homeDir, "Desktop") mpvShortcut := filepath.Join(desktopPath, constants.ShortcutMPVName) if wi.FileExists(mpvShortcut) { cr.outputChan <- "Removing MPV Desktop shortcut..." if err := os.Remove(mpvShortcut); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to remove %s: %v", constants.ShortcutMPVName, err) } else { cr.outputChan <- "Removed: " + constants.ShortcutMPVName + " (Desktop)" } } // Remove Start Menu shortcut appData := os.Getenv("APPDATA") if appData != "" { startMenuPath := filepath.Join(appData, "Microsoft", "Windows", "Start Menu", "Programs") startMenuShortcut := filepath.Join(startMenuPath, constants.ShortcutMPVName) if wi.FileExists(startMenuShortcut) { cr.outputChan <- "Removing MPV Start Menu shortcut..." if err := os.Remove(startMenuShortcut); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to remove Start Menu shortcut: %v", err) } else { cr.outputChan <- "Removed: " + constants.ShortcutMPVName + " (Start Menu)" } } } return nil }