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}}
Size
{{.Status.Length}} bytes
Piece Length
{{.Status.PieceLength}} bytes
Pieces
{{.Status.PieceCount}}
Files
{{len .Status.Files}}
Peers
{{.Status.PeerCount}}
Tracker
{{.Status.Announce}}
| File | Size (bytes) |
{{range .Status.Files}}
| {{.Path}} | {{.Length}} |
{{end}}
{{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)
}
}