package main import ( "embed" "encoding/json" "fmt" "io/fs" "log" "net" "net/http" "net/url" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "sync" "time" ) //go:embed web/ui/* var webUI embed.FS func cmdServe(args []string) { port := 0 host := "127.0.0.1" openBrowser := true for i := 0; i < len(args); i++ { switch args[i] { case "-p", "--port": if i+1 < len(args) { port, _ = strconv.Atoi(args[i+1]) i++ } case "--host": if i+1 < len(args) { host = args[i+1] i++ } case "--no-browser": openBrowser = false } } mux := http.NewServeMux() uiFS, err := fs.Sub(webUI, "web/ui") if err != nil { fatal("embed web/ui: %v", err) } mux.Handle("/", http.FileServer(http.FS(uiFS))) mux.HandleFunc("/api/search", handleSearch) mux.HandleFunc("/api/list", handleList) mux.HandleFunc("/api/list-wavs", handleListWavs) mux.HandleFunc("/api/download", handleAPIDownload) mux.HandleFunc("/api/extract", handleAPIExtract) mux.HandleFunc("/api/progress", handleProgress) wavsBase := "./output/wavs" if abs, err := filepath.Abs(wavsBase); err == nil { wavsBase = abs } if err := os.MkdirAll(wavsBase, 0755); err != nil { fatal("create wavs dir: %v", err) } mux.Handle("/wavs/", http.StripPrefix("/wavs/", http.FileServer(http.Dir(wavsBase)))) addr := fmt.Sprintf("%s:%d", host, port) listener, err := net.Listen("tcp", addr) if err != nil { fatal("listen: %v", err) } realPort := listener.Addr().(*net.TCPAddr).Port realHost := "0.0.0.0" if ip := listener.Addr().(*net.TCPAddr).IP; ip != nil { realHost = ip.String() } fmt.Printf("\n AKAI Utils Server\n") fmt.Printf(" =================\n\n") if realHost == "0.0.0.0" || realHost == "::" || realHost == "" || realHost == "" { fmt.Printf(" Local: http://localhost:%d\n", realPort) if realHost == "0.0.0.0" { fmt.Printf(" Network: http://:%d\n", realPort) } } else { fmt.Printf(" Local: http://%s:%d\n", realHost, realPort) } fmt.Printf(" API: http://localhost:%d/api/\n", realPort) fmt.Printf(" Quit: Ctrl+C\n\n") if openBrowser { go func() { time.Sleep(500 * time.Millisecond) openURL(fmt.Sprintf("http://localhost:%d", realPort)) }() } server := &http.Server{Handler: mux} log.Fatal(server.Serve(listener)) } func handleSearch(w http.ResponseWriter, r *http.Request) { q := r.URL.Query().Get("q") filter := r.URL.Query().Get("filter") limitStr := r.URL.Query().Get("limit") if q == "" { q = "subject:akai AND subject:sampler" } if filter != "" { q = "(" + q + ") AND (" + filter + ")" } limit := 30 if limitStr != "" { limit, _ = strconv.Atoi(limitStr) } results := searchArchive(q, limit) sort.Slice(results, func(i, j int) bool { return results[i].Downloads > results[j].Downloads }) writeJSON(w, results) } func handleList(w http.ResponseWriter, r *http.Request) { identifier := r.URL.Query().Get("identifier") if identifier == "" { http.Error(w, "missing identifier", http.StatusBadRequest) return } files := getItemFiles(identifier) writeJSON(w, files) } var ( downloadJobs = make(map[string]*downloadState) downloadJobsMu sync.Mutex ) type downloadState struct { ID string `json:"id"` Identifier string `json:"identifier"` FileName string `json:"file_name"` TotalSize int64 `json:"total_size"` Downloaded int64 `json:"downloaded"` Done bool `json:"done"` Error string `json:"error,omitempty"` Speed float64 `json:"speed"` lastCheck time.Time lastBytes int64 } func handleAPIDownload(w http.ResponseWriter, r *http.Request) { identifier := r.URL.Query().Get("identifier") outDir := r.URL.Query().Get("dir") if identifier == "" { http.Error(w, "missing identifier", http.StatusBadRequest) return } if outDir == "" { outDir = "." } if abs, err := filepath.Abs(outDir); err == nil { outDir = abs } if err := os.MkdirAll(outDir, 0755); err != nil { writeJSON(w, map[string]any{"error": fmt.Sprintf("create %s: %v", outDir, err)}) return } files := getItemFiles(identifier) if len(files) == 0 { writeJSON(w, map[string]any{"error": "no ISO files found"}) return } var jobIDs []string for _, f := range files { size, _ := strconv.ParseInt(f.Size, 10, 64) jobID := identifier + "/" + f.Name state := &downloadState{ ID: jobID, Identifier: identifier, FileName: f.Name, TotalSize: size, lastCheck: time.Now(), } downloadJobsMu.Lock() downloadJobs[jobID] = state downloadJobsMu.Unlock() jobIDs = append(jobIDs, jobID) go func(j downloadJob, s *downloadState) { if err := os.MkdirAll(filepath.Dir(j.OutPath), 0755); err != nil { s.Error = err.Error() return } client := &http.Client{Timeout: 0} req, _ := http.NewRequest("GET", j.URL, nil) req.Header.Set("User-Agent", userAgent) if fi, err := os.Stat(j.OutPath); err == nil && j.Size > 0 && fi.Size() == j.Size { s.Downloaded = j.Size s.Done = true return } resp, err := client.Do(req) if err != nil { s.Error = err.Error() return } defer func() { _ = resp.Body.Close() }() outFile, err := os.Create(j.OutPath) if err != nil { s.Error = err.Error() return } defer func() { _ = outFile.Close() }() buf := make([]byte, 32*1024) for { n, readErr := resp.Body.Read(buf) if n > 0 { if _, err := outFile.Write(buf[:n]); err != nil { s.Error = err.Error() return } s.Downloaded += int64(n) now := time.Now() elapsed := now.Sub(s.lastCheck).Seconds() if elapsed >= 1.0 { s.Speed = float64(s.Downloaded-s.lastBytes) / elapsed s.lastCheck = now s.lastBytes = s.Downloaded } } if readErr != nil { break } } s.Done = true s.Speed = 0 }(downloadJob{ Identifier: identifier, Name: f.Name, URL: fmt.Sprintf("%s/download/%s/%s", iaBaseURL, url.PathEscape(identifier), url.PathEscape(f.Name)), OutPath: filepath.Join(outDir, f.Name), Size: size, }, state) } writeJSON(w, map[string]any{"jobs": jobIDs}) } func handleListWavs(w http.ResponseWriter, r *http.Request) { dir := r.URL.Query().Get("dir") if dir == "" { dir = "./output/wavs" } if abs, err := filepath.Abs(dir); err == nil { dir = abs } entries, err := os.ReadDir(dir) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } files := []string{} for _, e := range entries { if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".wav") { files = append(files, e.Name()) } } writeJSON(w, files) } func handleProgress(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming unsupported", http.StatusInternalServerError) return } ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for range ticker.C { downloadJobsMu.Lock() data, _ := json.Marshal(downloadJobs) downloadJobsMu.Unlock() if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil { return } flusher.Flush() allDone := true downloadJobsMu.Lock() for _, s := range downloadJobs { if !s.Done && s.Error == "" { allDone = false break } } downloadJobsMu.Unlock() if allDone && len(downloadJobs) > 0 { break } } } func handleAPIExtract(w http.ResponseWriter, r *http.Request) { isoDir := r.URL.Query().Get("dir") outDir := r.URL.Query().Get("out") if isoDir == "" { isoDir = "." } if outDir == "" { outDir = "./wavs" } entries, err := os.ReadDir(isoDir) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } var isos []string for _, e := range entries { if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".iso") { isos = append(isos, filepath.Join(isoDir, e.Name())) } } if len(isos) == 0 { writeJSON(w, map[string]any{"error": "no ISO files found in " + isoDir}) return } var messages []string for _, isoPath := range isos { result, err := extractISO(isoPath, outDir) if err != nil { messages = append(messages, fmt.Sprintf("FAIL %s: %v", filepath.Base(isoPath), err)) } else { messages = append(messages, fmt.Sprintf("OK %s: %s", filepath.Base(isoPath), result)) } } writeJSON(w, map[string]any{ "message": fmt.Sprintf("Extracted %d ISOs: %s", len(isos), strings.Join(messages, "; ")), }) } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") if err := json.NewEncoder(w).Encode(v); err != nil { // client disconnected, nothing we can do return } } func openURL(rawURL string) { _ = exec.Command("xdg-open", rawURL).Start() _ = exec.Command("open", rawURL).Start() _ = exec.Command("rundll32", "url.dll,FileProtocolHandler", rawURL).Start() } func init() { log.SetFlags(0) }