package web import ( "bufio" "fmt" "os" "os/exec" "runtime" "strings" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" ) // cleanVersionString removes package name prefix from version string // e.g., "mpv 1:0.41.0-2.1" → "1:0.41.0-2.1" func cleanVersionString(version, packageName string) string { if version == "" { return "" } // If version starts with package name followed by space, remove it prefix := packageName + " " if strings.HasPrefix(version, prefix) { return strings.TrimPrefix(version, prefix) } return version } // getPackageNameForMethod maps method ID to package name for version checking func getPackageNameForMethod(methodID string) string { switch { case strings.Contains(methodID, constants.MethodMPVPackage), strings.Contains(methodID, constants.MethodMPVFlatpak), strings.Contains(methodID, constants.MethodMPVBrew): return "mpv" case strings.Contains(methodID, constants.MethodCelluloidPackage), strings.Contains(methodID, constants.MethodCelluloidFlatpak): return "celluloid" default: return "" } } // GetPackageVersion queries the installed version of a package using the appropriate package manager func GetPackageVersion(methodID, appName string) string { // Map method ID to package name packageName := getPackageNameForMethod(methodID) switch { case strings.Contains(methodID, "flatpak"): return getFlatpakPackageVersion(packageName) case strings.Contains(methodID, "brew"): return getBrewPackageVersion(packageName) case strings.Contains(methodID, "package"): return getLinuxPackageVersion(packageName) default: return "" } } // GetAvailableVersion queries the available version of a package from the appropriate package manager func GetAvailableVersion(methodID, appName string) string { // Map method ID to package name packageName := getPackageNameForMethod(methodID) switch { case strings.Contains(methodID, "flatpak"): return getFlatpakAvailableVersion(packageName) case strings.Contains(methodID, "package"): // For pacman-based systems, query available version if runtime.GOOS == "linux" { if _, err := os.Stat("/etc/os-release"); err == nil { file, _ := os.Open("/etc/os-release") var id, idLike string scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "ID=") { id = strings.Trim(strings.TrimPrefix(line, "ID="), `"`) } else if strings.HasPrefix(line, "ID_LIKE=") { idLike = strings.Trim(strings.TrimPrefix(line, "ID_LIKE="), `"`) } } file.Close() id = strings.ToLower(id) idLike = strings.ToLower(idLike) archFamily := []string{"arch", "manjaro", "endeavouros", "garuda", "cachyos"} for _, distro := range archFamily { if id == distro || strings.Contains(idLike, distro) { return getPacmanAvailableVersion(packageName) } } } } return "" default: return "" } } // isPackageManagerMethod checks if the method ID represents a package manager installation func isPackageManagerMethod(methodID string) bool { pmMethods := []string{ "mpv-package", "mpv-flatpak", "mpv-brew", "celluloid-package", "celluloid-flatpak", } for _, m := range pmMethods { if strings.HasSuffix(methodID, m) || methodID == m { return true } } return false } // getFlatpakPackageName returns the flatpak package name for an app func getFlatpakPackageName(appName string) string { appName = strings.ToLower(appName) switch appName { case "mpv": return constants.FlatpakMPV case "celluloid": return constants.FlatpakCelluloid default: return appName } } // getFlatpakAppID returns the full Flatpak app ID for a package func getFlatpakAppID(packageName string) string { packageName = strings.ToLower(packageName) switch packageName { case "mpv": return constants.FlatpakMPV case "celluloid": return constants.FlatpakCelluloid default: return packageName } } // getBrewPackageName returns the brew package name for an app func getBrewPackageName(appName string) string { appName = strings.ToLower(appName) switch appName { case "mpv": return "mpv" default: return appName } } // getAptPackageName returns the apt package name for an app func getAptPackageName(appName string) string { appName = strings.ToLower(appName) switch appName { case "mpv": return "mpv" case "celluloid": return "celluloid" default: return appName } } // getLinuxPackageVersion queries the appropriate Linux package manager for the installed version func getLinuxPackageVersion(packageName string) string { if runtime.GOOS != "linux" { return "" } // Try to detect distro and use appropriate package manager if _, err := os.Stat("/etc/os-release"); err == nil { file, err := os.Open("/etc/os-release") if err != nil { return "" } defer file.Close() var id, idLike string scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "ID=") { id = strings.Trim(strings.TrimPrefix(line, "ID="), `"`) } else if strings.HasPrefix(line, "ID_LIKE=") { idLike = strings.Trim(strings.TrimPrefix(line, "ID_LIKE="), `"`) } } id = strings.ToLower(id) idLike = strings.ToLower(idLike) // Check for Arch-based distros archFamily := []string{"arch", "manjaro", "endeavouros", "garuda", "cachyos"} for _, distro := range archFamily { if id == distro || strings.Contains(idLike, distro) { return getPacmanPackageVersion(packageName) } } // Check for Debian-based distros debianFamily := []string{"debian", "ubuntu", "linuxmint", "mint", "pop", "elementaryos"} for _, distro := range debianFamily { if id == distro || strings.Contains(idLike, distro) { return getAptPackageVersion(packageName) } } // Check for RHEL-based distros rhelFamily := []string{"rhel", "fedora", "centos", "redhat", "rocky", "almalinux"} for _, distro := range rhelFamily { if id == distro || strings.Contains(idLike, distro) { return getDnfPackageVersion(packageName) } } // Check for SUSE-based distros suseFamily := []string{"opensuse", "suse"} for _, distro := range suseFamily { if id == distro || strings.Contains(idLike, distro) { return getZypperPackageVersion(packageName) } } // Default to trying apt return getAptPackageVersion(packageName) } return "" } // tryPacmanVersion attempts to get the installed version of a package using pacman -Qi // Returns empty string if the package is not found or an error occurs func tryPacmanVersion(packageName string) string { cmd := exec.Command("pacman", "-Qi", packageName) output, err := cmd.CombinedOutput() if err != nil { return "" } // Parse output for "Version : X.Y.Z" - be more specific to avoid matching lines with "Version" in other contexts scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // Check for "Version" followed by colon to ensure we're parsing the correct line if strings.HasPrefix(line, "Version") && strings.Contains(line, ":") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { version := strings.TrimSpace(parts[1]) // Additional check: version should not contain package name // e.g., "mpv 0.41.0-2.1" should only return "0.41.0-2.1" if strings.Contains(version, " ") { // If version contains space, take the last part (actual version number) versionParts := strings.Fields(version) if len(versionParts) > 0 { version = versionParts[len(versionParts)-1] } } // Clean version string by removing package name prefix (defensive measure) return cleanVersionString(version, packageName) } } } return "" } // getPacmanPackageVersion queries pacman for the installed version of a package // For celluloid, tries alternative package names (celluloid-git, gnome-mpv, gnome-mpv-git) func getPacmanPackageVersion(packageName string) string { // Check if pacman is available cmd := exec.Command("which", "pacman") if err := cmd.Run(); err != nil { log.Info("pacman not available") return "" } // Try primary package name version := tryPacmanVersion(packageName) if version != "" { return version } // For celluloid, try alternative package names (AUR variants, old name) if packageName == "celluloid" { alternatives := []string{"celluloid-git", "gnome-mpv", "gnome-mpv-git"} for _, alt := range alternatives { log.Info(fmt.Sprintf("Celluloid not found under primary package name, trying: %s", alt)) version = tryPacmanVersion(alt) if version != "" { log.Info(fmt.Sprintf("Found Celluloid under package name: %s", alt)) return version } } log.Info("Celluloid not found under any package name (celluloid, celluloid-git, gnome-mpv, gnome-mpv-git)") } return "" } // getPacmanAvailableVersion queries pacman for the available version of a package in the repos func getPacmanAvailableVersion(packageName string) string { // Check if pacman is available cmd := exec.Command("which", "pacman") if err := cmd.Run(); err != nil { log.Info("pacman not available") return "" } // Use pacman -Si to get available version cmd = exec.Command("pacman", "-Si", packageName) output, err := cmd.CombinedOutput() if err != nil { log.Debug(fmt.Sprintf("Failed to get pacman available version for %s: %v", packageName, err)) return "" } // Parse output for "Version : X.Y.Z" scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "Version") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { version := strings.TrimSpace(parts[1]) // Clean version string by removing package name prefix (defensive measure) return cleanVersionString(version, packageName) } } } return "" } // getDnfPackageVersion queries dnf for the installed version of a package func getDnfPackageVersion(packageName string) string { // Check if dnf is available cmd := exec.Command("which", "dnf") if err := cmd.Run(); err != nil { log.Info("dnf not available") return "" } cmd = exec.Command("dnf", "info", "--installed", packageName) output, err := cmd.CombinedOutput() if err != nil { log.Debug(fmt.Sprintf("Failed to get dnf version for %s: %v", packageName, err)) return "" } scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // Check for "Version" followed by colon to ensure we're parsing correct line if strings.HasPrefix(line, "Version") && strings.Contains(line, ":") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { version := strings.TrimSpace(parts[1]) // Additional check: version should not contain package name if strings.Contains(version, " ") { // If version contains space, take the last part (actual version number) versionParts := strings.Fields(version) if len(versionParts) > 0 { version = versionParts[len(versionParts)-1] } } // Clean version string by removing package name prefix (defensive measure) return cleanVersionString(version, packageName) } } } return "" } // getZypperPackageVersion queries zypper (via rpm) for the installed version of a package func getZypperPackageVersion(packageName string) string { // Check if rpm is available (zypper uses rpm for package queries) cmd := exec.Command("which", "rpm") if err := cmd.Run(); err != nil { log.Info("rpm not available") return "" } cmd = exec.Command("rpm", "-q", "--queryformat", "%{VERSION}-%{RELEASE}", packageName) output, err := cmd.Output() if err != nil { log.Debug(fmt.Sprintf("Failed to get zypper/rpm version for %s: %v", packageName, err)) return "" } version := strings.TrimSpace(string(output)) if version != "" && !strings.Contains(version, "not installed") { log.Debug(fmt.Sprintf("Found zypper version for %s: %s", packageName, version)) return version } return "" } // getAptPackageVersion queries apt for the installed version of a package func getAptPackageVersion(packageName string) string { // Check if apt is available cmd := exec.Command("which", "apt-get") if err := cmd.Run(); err != nil { log.Info("apt-get not available") return "" } cmd = exec.Command("apt-cache", "policy", packageName) output, err := cmd.CombinedOutput() if err != nil { log.Debug(fmt.Sprintf("Failed to get apt version for %s: %v", packageName, err)) return "" } scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "Installed:") && strings.Contains(line, ":") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { version := strings.TrimSpace(parts[1]) if version != "(none)" { // Additional check: version should not contain package name if strings.Contains(version, " ") { // If version contains space, take the last part (actual version number) versionParts := strings.Fields(version) if len(versionParts) > 0 { version = versionParts[len(versionParts)-1] } } // Clean version string by removing package name prefix (defensive measure) return cleanVersionString(version, packageName) } } } } return "" } // getFlatpakPackageVersion queries flatpak for the installed version of an app func getFlatpakPackageVersion(packageName string) string { // Check if flatpak is available cmd := exec.Command("which", "flatpak") if err := cmd.Run(); err != nil { log.Info("flatpak not available") return "" } // Map to proper Flatpak app ID appID := getFlatpakAppID(packageName) // Use flatpak info to get installed version cmd = exec.Command("flatpak", "info", appID) output, err := cmd.CombinedOutput() if err != nil { log.Debug(fmt.Sprintf("Failed to get flatpak version for %s: %v", appID, err)) return "" } // Parse output for "Version: X.Y.Z" (exact match, not "Runtime Version:" or "Sdk Version:") scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // Exact match for "Version:" line (not Runtime Version or Sdk Version) if line == "Version:" || strings.HasPrefix(line, "Version: ") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { version := strings.TrimSpace(parts[1]) // Clean version string by removing package name prefix (defensive measure) return cleanVersionString(version, packageName) } } } return "" } // getFlatpakAvailableVersion queries flatpak remote for the available version of an app func getFlatpakAvailableVersion(packageName string) string { // Check if flatpak is available cmd := exec.Command("which", "flatpak") if err := cmd.Run(); err != nil { log.Info("flatpak not available") return "" } // Map to proper Flatpak app ID appID := getFlatpakAppID(packageName) // Use flathub remote directly for available version check cmd = exec.Command("flatpak", "remote-info", "flathub", appID) output, err := cmd.CombinedOutput() if err != nil { log.Debug(fmt.Sprintf("Failed to get flatpak available version for %s: %v", appID, err)) return "" } // Parse output for "Version: X.Y.Z" scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "Version:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { version := strings.TrimSpace(parts[1]) // Clean version string by removing package name prefix (defensive measure) return cleanVersionString(version, packageName) } } } return "" } // getBrewPackageVersion queries brew for the installed version of a formula func getBrewPackageVersion(packageName string) string { if runtime.GOOS != "darwin" { return "" } // Check if brew is available cmd := exec.Command("which", "brew") if err := cmd.Run(); err != nil { log.Info("brew not available") return "" } cmd = exec.Command("brew", "list", "--versions", packageName) output, err := cmd.CombinedOutput() if err != nil { log.Debug(fmt.Sprintf("Failed to get brew version for %s: %v", packageName, err)) return "" } scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, packageName) { parts := strings.Fields(line) if len(parts) >= 2 { version := parts[1] // Clean version string by removing package name prefix (defensive measure) return cleanVersionString(version, packageName) } } } return "" }