31fd368bda
Some archive.org file names include directory prefixes (e.g. "ZERO-G - Deepest India/vocal.iso"). MkdirAll only created the output root, not intermediate directories. Added filepath.Dir(MkdirAll) in both downloadFile (CLI) and handleAPIDownload (web).
346 lines
7.8 KiB
Go
346 lines
7.8 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/download", handleAPIDownload)
|
|
mux.HandleFunc("/api/extract", handleAPIExtract)
|
|
mux.HandleFunc("/api/progress", handleProgress)
|
|
|
|
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) {
|
|
os.MkdirAll(filepath.Dir(j.OutPath), 0755)
|
|
|
|
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 resp.Body.Close()
|
|
|
|
outFile, err := os.Create(j.OutPath)
|
|
if err != nil {
|
|
s.Error = err.Error()
|
|
return
|
|
}
|
|
defer outFile.Close()
|
|
|
|
buf := make([]byte, 32*1024)
|
|
for {
|
|
n, readErr := resp.Body.Read(buf)
|
|
if n > 0 {
|
|
outFile.Write(buf[:n])
|
|
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 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()
|
|
|
|
fmt.Fprintf(w, "data: %s\n\n", data)
|
|
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", "*")
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
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)
|
|
}
|