package ui import ( "errors" "fmt" "html/template" "io" "net" "net/http" "os" "os/exec" "runtime" "strings" "github.com/veggiedefender/torrent-client/internal/app" "github.com/veggiedefender/torrent-client/internal/torrent" ) type webUI struct { controller *app.Controller } type pageData struct { Status torrent.Status Message string Error string } var pageTemplate = template.Must(template.New("index").Parse(` Ztorrent

Ztorrent

Load a torrent by path or upload .torrent file

{{if .Message}}
{{.Message}}
{{end}} {{if .Error}}
{{.Error}}
{{end}}

Status

{{if .Status.Loaded}}
Name
{{.Status.Name}}
Size
{{.Status.Length}} bytes
Piece Length
{{.Status.PieceLength}} bytes
Pieces
{{.Status.PieceCount}}
Files
{{len .Status.Files}}
Peers
{{.Status.PeerCount}}
Tracker
{{.Status.Announce}}
{{range .Status.Files}} {{end}}
FileSize (bytes)
{{.Path}}{{.Length}}
{{else}}

No torrent loaded yet.

{{end}}
`)) func Start(controller *app.Controller) { ui := &webUI{controller: controller} mux := http.NewServeMux() mux.HandleFunc("/", ui.handleIndex) mux.HandleFunc("/load", ui.handleLoad) listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { fmt.Printf("Failed to start GUI listener: %v\n", err) return } defer listener.Close() guiURL := "http://" + listener.Addr().String() fmt.Printf("Ztorrent GUI started at %s\n", guiURL) openBrowser(guiURL) if err := http.Serve(listener, mux); err != nil && !errors.Is(err, http.ErrServerClosed) { fmt.Printf("GUI server error: %v\n", err) } } func (ui *webUI) handleIndex(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } ui.renderPage(w, pageData{Status: ui.controller.Status()}) } func (ui *webUI) handleLoad(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseMultipartForm(32 << 20); err != nil { ui.renderPage(w, pageData{ Status: ui.controller.Status(), Error: fmt.Sprintf("failed to parse form: %v", err), }) return } path := strings.TrimSpace(r.FormValue("path")) var cleanupPath string if path == "" { uploadedPath, err := saveUploadedTorrent(r) if err != nil { ui.renderPage(w, pageData{ Status: ui.controller.Status(), Error: fmt.Sprintf("failed to read uploaded file: %v", err), }) return } path = uploadedPath cleanupPath = uploadedPath } if cleanupPath != "" { defer os.Remove(cleanupPath) } loadErr := ui.controller.StartTorrent(path) status := ui.controller.Status() data := pageData{ Status: status, } if loadErr != nil { data.Error = loadErr.Error() if status.Loaded { data.Message = fmt.Sprintf("Metadata loaded from %s, but tracker announce failed", path) } } else { data.Message = fmt.Sprintf("Loaded torrent from %s", path) } ui.renderPage(w, data) } func (ui *webUI) renderPage(w http.ResponseWriter, data pageData) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := pageTemplate.Execute(w, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func saveUploadedTorrent(r *http.Request) (string, error) { file, _, err := r.FormFile("torrent_file") if err != nil { return "", err } defer file.Close() tmp, err := os.CreateTemp("", "ztorrent-*.torrent") if err != nil { return "", err } defer tmp.Close() if _, err := io.Copy(tmp, file); err != nil { return "", err } return tmp.Name(), nil } func openBrowser(targetURL string) { var cmd *exec.Cmd switch runtime.GOOS { case "windows": cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", targetURL) case "darwin": cmd = exec.Command("open", targetURL) default: cmd = exec.Command("xdg-open", targetURL) } if err := cmd.Start(); err != nil { fmt.Printf("Failed to open browser automatically: %v\nOpen manually: %s\n", err, targetURL) } }