initial commit
Marco Andronaco andronacomarco@gmail.com
Wed, 13 Nov 2024 14:49:42 +0100
9 files changed,
276 insertions(+),
0 deletions(-)
A
Dockerfile
@@ -0,0 +1,30 @@
+# syntax=docker/dockerfile:1 + +FROM golang:1.23-alpine AS builder + +WORKDIR /build + +# Download Go modules +COPY go.mod . +RUN go mod download +RUN go mod verify + +# Transfer source code +COPY *.go . +COPY *.html . + +# Build +RUN CGO_ENABLED=0 go build -trimpath -o /dist/app + +# Test +FROM builder AS run-test-stage +RUN go test -v ./... + +FROM alpine AS build-release-stage + +RUN apk add --no-cache ffmpeg + +WORKDIR /app + +COPY --from=builder /dist . +ENTRYPOINT ["./app"]
A
LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT) + +Copyright (c) 2024 BiRabittoh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
A
compose.yaml
@@ -0,0 +1,10 @@
+name: guesser + +services: + app: + build: . + image: ghcr.io/birabittoh/guesser:main + container_name: guesser + restart: unless-stopped + ports: + - 3000:3000
A
index.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html> +<html lang="it"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Guess the Song</title> + <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"> +</head> +<body> + + <header> + <h1>Guess the Song</h1> + <p>Generatore di reel per Guess the Song. Si prega di non abusare.</p> + </header> + + <main> + <form action="/" method="POST" enctype="multipart/form-data" id="videoForm"> + <label for="videoFile">Base video (es: guess.mp4):</label> + <input type="file" id="videoFile" name="videoFile" accept="video/*" required> + + <label for="audioFile">Canzone (es: song.opus):</label> + <input type="file" id="audioFile" name="audioFile" accept="audio/*" required> + + <label for="skip">Tempo di partenza della riproduzione audio (secondi):</label> + <input type="number" id="skip" name="skip" step="0.1" value="0" required> + + <hr /> + + <label for="fade">Durata del fade-in e fade-out (secondi):</label> + <input type="number" id="fade" name="fade" step="0.1" value="1" required> + + <label for="delay">Ritardo prima dell'inizio dell'audio (secondi):</label> + <input type="number" id="delay" name="delay" step="0.1" value="3" required> + + <label for="duration">Durata della riproduzione dell'audio (secondi):</label> + <input type="number" id="duration" name="duration" step="0.1" value="14" required> + + <p style="text-align: center;"> + <button type="submit" onclick="this.disabled = true; document.getElementById('videoForm').submit();">Elabora</button> + </p> + </form> + </main> + + <footer> + <p>© <a href="https://linktr.ee/earthboundcafe">EarthBound Café</a>, realizzato da <a href="https://birabittoh.is-a.dev/">BiRabittoh</a>.</p> + </footer> + +</body> +</html>
A
main.go
@@ -0,0 +1,32 @@
+package main + +import ( + "embed" + "fmt" + "net/http" +) + +//go:embed index.html +var indexPage embed.FS + +func setupRoutes() { + http.HandleFunc("GET /", serveHTML) + http.HandleFunc("POST /", processHandler) +} + +func main() { + setupRoutes() + fmt.Println("Server in ascolto su :3000") + http.ListenAndServe(":3000", nil) +} + +func serveHTML(w http.ResponseWriter, r *http.Request) { + html, err := indexPage.ReadFile("index.html") + if err != nil { + http.Error(w, "Errore durante il caricamento del form HTML", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html") + w.Write(html) +}
A
merge.go
@@ -0,0 +1,128 @@
+package main + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strconv" + "strings" +) + +func saveUploadedFile(file io.Reader, path string) error { + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, file) + return err +} + +func processHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Metodo non supportato", http.StatusMethodNotAllowed) + return + } + + // Parse del form per ottenere i file e gli altri parametri + if err := r.ParseMultipartForm(10 << 20); err != nil { + http.Error(w, "Errore parsing form", http.StatusBadRequest) + return + } + + // Recupera i file audio e video + audioFile, _, err := r.FormFile("audioFile") + if err != nil { + http.Error(w, "File audio mancante", http.StatusBadRequest) + return + } + defer audioFile.Close() + + videoFile, _, err := r.FormFile("videoFile") + if err != nil { + http.Error(w, "File video mancante", http.StatusBadRequest) + return + } + defer videoFile.Close() + + // Salva i file temporaneamente + audioPath := "temp_audio.opus" + videoPath := "temp_video.mp4" + outputPath := "output.mp4" + + if err := saveUploadedFile(audioFile, audioPath); err != nil { + http.Error(w, "Errore nel salvataggio del file audio", http.StatusInternalServerError) + return + } + if err := saveUploadedFile(videoFile, videoPath); err != nil { + http.Error(w, "Errore nel salvataggio del file video", http.StatusInternalServerError) + return + } + + // Recupera i parametri di configurazione dal form + skip, _ := strconv.ParseFloat(r.FormValue("skip"), 64) + fade, _ := strconv.ParseFloat(r.FormValue("fade"), 64) + delay, _ := strconv.ParseFloat(r.FormValue("delay"), 64) + duration, _ := strconv.ParseFloat(r.FormValue("duration"), 64) + + // Conversione delay in millisecondi + delayMs := int(delay * 1000) + fadeOutStart := duration - fade + + // Costruzione del filtro audio + audioFilter := fmt.Sprintf("afade=t=out:st=%.2f:d=%.2f", fadeOutStart, fade) + if skip >= fade { + audioFilter = fmt.Sprintf("afade=t=in:ss=0:d=%.2f,%s", fade, audioFilter) + } + + // Costruzione del comando ffmpeg + ffmpegCommand := []string{ + "-i", videoPath, + "-i", audioPath, + "-filter_complex", + fmt.Sprintf("[1:a]atrim=start=%.2f:end=%.2f,asetpts=PTS-STARTPTS,%s,adelay=%d|%d[song];[0:a][song]amix=inputs=2:duration=first[audio_mix];[0:v]copy[v]", + skip, skip+duration, audioFilter, delayMs, delayMs), + "-map", "[v]", "-map", "[audio_mix]", + "-c:v", "libx264", + "-c:a", "aac", + "-shortest", outputPath, + "-y", + } + + // Esecuzione del comando ffmpeg + cmd := exec.Command("ffmpeg", ffmpegCommand...) + println(strings.Join(ffmpegCommand, " ")) + cmd.Stderr = os.Stderr // Mostra eventuali errori in console + cmd.Stdout = os.Stdout + + if err := cmd.Run(); err != nil { + http.Error(w, "Errore durante l'esecuzione di ffmpeg", http.StatusInternalServerError) + return + } + + // Impostazioni per scaricare il file di output + w.Header().Set("Content-Disposition", "attachment; filename=output.mp4") + w.Header().Set("Content-Type", "video/mp4") + + // Restituisce il file output.mp4 in risposta + outputFile, err := os.Open(outputPath) + if err != nil { + http.Error(w, "Errore nell'apertura del file output", http.StatusInternalServerError) + return + } + defer outputFile.Close() + + _, err = io.Copy(w, outputFile) + if err != nil { + http.Error(w, "Errore durante la copia del file", http.StatusInternalServerError) + return + } + + // Pulizia dei file temporanei + os.Remove(audioPath) + os.Remove(videoPath) + os.Remove(outputPath) +}