Files
akai-utils/serve.go
T

463 lines
11 KiB
Go

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/disk/partitions", handleDiskPartitions)
mux.HandleFunc("/api/disk/volumes", handleDiskVolumes)
mux.HandleFunc("/api/volume/tags", handleVolumeTags)
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 == "<nil>" {
fmt.Printf(" Local: http://localhost:%d\n", realPort)
if realHost == "0.0.0.0" {
fmt.Printf(" Network: http://<host-ip>:%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
}
files := []string{}
var walkErr error
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
if path == dir {
walkErr = err
return filepath.SkipAll
}
return nil
}
if !d.IsDir() && strings.HasSuffix(strings.ToLower(d.Name()), ".wav") {
rel, _ := filepath.Rel(dir, path)
files = append(files, rel)
}
return nil
})
if walkErr != nil {
writeJSON(w, map[string]any{"error": walkErr.Error()})
return
}
if err != nil {
writeJSON(w, map[string]any{"error": err.Error()})
return
}
writeJSON(w, files)
}
func handleDiskPartitions(w http.ResponseWriter, r *http.Request) {
iso := r.URL.Query().Get("iso")
if iso == "" {
writeJSON(w, map[string]any{"error": "missing iso parameter"})
return
}
parts, err := listPartitions(iso)
if err != nil {
writeJSON(w, map[string]any{"error": err.Error()})
return
}
writeJSON(w, parts)
}
func handleDiskVolumes(w http.ResponseWriter, r *http.Request) {
iso := r.URL.Query().Get("iso")
part := r.URL.Query().Get("part")
if iso == "" || part == "" {
writeJSON(w, map[string]any{"error": "missing iso or part parameter"})
return
}
parts, err := listPartitions(iso)
if err != nil {
writeJSON(w, map[string]any{"error": err.Error()})
return
}
vols, err := listVolumes(iso, parts)
if err != nil {
writeJSON(w, map[string]any{"error": err.Error()})
return
}
filtered := []volInfo{}
for _, v := range vols {
if v.Part == part {
filtered = append(filtered, v)
}
}
writeJSON(w, filtered)
}
func handleVolumeTags(w http.ResponseWriter, r *http.Request) {
iso := r.URL.Query().Get("iso")
part := r.URL.Query().Get("part")
vol := r.URL.Query().Get("vol")
if iso == "" || part == "" || vol == "" {
writeJSON(w, map[string]any{"error": "missing iso, part, or vol parameter"})
return
}
tags, err := listTags(iso, part, vol)
if err != nil {
writeJSON(w, map[string]any{"error": err.Error()})
return
}
writeJSON(w, tags)
}
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)
}