feat: enhance update feedback with loading and success messages in forms and video processing

This commit is contained in:
Marco Pedone 2026-03-02 16:33:54 +01:00
parent c31a68ef6d
commit f1bd40afce
4 changed files with 77 additions and 26 deletions

View file

@ -167,6 +167,7 @@ func (s *Server) SetupRoutes() *echo.Echo {
return c.String(http.StatusInternalServerError, err.Error())
}
*/
log.Println("Update completed")
return c.String(http.StatusOK, "Updated")
})
e.GET("/update-cod/:cod", func(c echo.Context) error {
@ -207,6 +208,7 @@ func (s *Server) SetupRoutes() *echo.Echo {
return c.String(http.StatusInternalServerError, err.Error())
}
*/
log.Printf("Update %s completed", cod)
return c.String(http.StatusOK, "Updated")
})

View file

@ -197,42 +197,65 @@ func VideoConversion(input string) (string, string, error) {
ext := filepath.Ext(basename)
nameWithoutExt := basename[0 : len(basename)-len(ext)]
tempVideo := filepath.Join(outputdir, nameWithoutExt+"_temp.mp4")
// Final destinations on the (potentially overlayfs) volume
videoOutput := filepath.Join(outputdir, nameWithoutExt+".mp4")
thumbOutput := filepath.Join(outputdir, "thumbnail-"+nameWithoutExt+".webp")
// 1. Transcode video to H.264/AAC MP4 (web-optimized)
//log.Println("Transcoding video:", input)
err := transcodeVideo(input, tempVideo)
// Use /tmp for all ffmpeg work: /tmp is tmpfs and supports random-access
// seeks that overlayfs does not, which is required for MP4 muxing and
// for the faststart moov-atom relocation.
tmpVideo := filepath.Join(os.TempDir(), nameWithoutExt+"_converted.mp4")
tmpThumb := filepath.Join(os.TempDir(), "thumbnail-"+nameWithoutExt+".webp")
defer os.Remove(tmpVideo)
defer os.Remove(tmpThumb)
// 1. Transcode video to H.264/AAC MP4 (web-optimized) into /tmp
err := transcodeVideo(input, tmpVideo)
if err != nil {
return "", "", fmt.Errorf("failed to transcode video: %w", err)
}
// 2. Extract thumbnail from video at 1 second
//log.Println("Extracting thumbnail from:", input)
err = extractThumbnail(input, thumbOutput)
// 2. Extract thumbnail from original video into /tmp
err = extractThumbnail(input, tmpThumb)
if err != nil {
return "", "", fmt.Errorf("failed to extract thumbnail: %w", err)
}
// 3. Move completed files from /tmp to the videos volume
os.Remove(input)
err = os.Rename(tempVideo, videoOutput)
if err != nil {
return "", "", fmt.Errorf("failed to rename converted video: %w", err)
if err = copyFile(tmpVideo, videoOutput); err != nil {
return "", "", fmt.Errorf("failed to move converted video: %w", err)
}
if err = copyFile(tmpThumb, thumbOutput); err != nil {
return "", "", fmt.Errorf("failed to move thumbnail: %w", err)
}
return videoOutput, thumbOutput, nil
}
// transcodeVideo converts video to web-optimized MP4
// copyFile copies src to dst, creating dst if needed.
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err = io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}
// transcodeVideo converts video to web-optimized MP4.
// output must be on a filesystem that supports random-access seeks (e.g. /tmp),
// because the MP4 muxer and -movflags +faststart both require re-opening the
// output file to relocate the moov atom — this fails on Docker overlayfs.
func transcodeVideo(input, output string) error {
// FFmpeg command for web optimization:
// -c:v libx264: H.264 codec (widely supported)
// -preset fast: encoding speed/quality tradeoff
// -crf 23: quality (lower = better, 18-28 recommended)
// -c:a aac: AAC audio codec
// -b:a 128k: audio bitrate
// -movflags +faststart: enable streaming before full download
// -vf scale=1280:-2: scale to max width 1280, maintain aspect ratio
cmd := exec.Command("ffmpeg",
"-i", input,
"-c:v", "libx264",
@ -241,7 +264,7 @@ func transcodeVideo(input, output string) error {
"-c:a", "aac",
"-b:a", "128k",
"-movflags", "+faststart",
"-vf", "scale=min(1280\\,iw):-2", // Escape comma for cross-platform
"-vf", "scale=min(1280\\,iw):-2",
"-y",
output,
)

View file

@ -141,15 +141,26 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => {
});
const { mutateAsync: update } = api.revalidation.updateAnnuncio.useMutation({
onMutate: () => {
toast.loading("Richiesta di aggiornamento inviata, attendere prego...", {
id: "update-annuncio",
});
},
onSettled: async () => {
await utils.annunci.getAnnuncio.invalidate({ cod: data.codice });
await utils.annunci.getAnnuncioById_rawImgUrls.invalidate({
id: data.id,
});
toast.success(
"L'aggiornamento dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.",
toast.success("Aggiornamento completato", {
id: "update-annuncio",
});
},
onError: async (err) => {
toast.error(
"Si è verificato un errore durante l'aggiornamento degli annunci: " +
(err instanceof Error ? err.message : "Errore sconosciuto"),
{
duration: 5000,
id: "update-annuncio-all",
},
);
},

View file

@ -21,12 +21,27 @@ const Admin_Annunci: NextPageWithLayout = () => {
const utils = api.useUtils();
const { mutateAsync: update } = api.revalidation.updateAllAnnunci.useMutation(
{
onMutate: () => {
toast.loading(
"Richiesta di aggiornamento inviata, attendere prego...",
{
id: "update-annuncio-all",
},
);
},
onSettled: async () => {
await utils.annunci.getAnnunciList.invalidate();
toast.success(
"La revalidazione dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.",
toast.success("Aggiornamento completato", {
id: "update-annuncio-all",
});
},
onError: async (err) => {
toast.error(
"Si è verificato un errore durante l'aggiornamento degli annunci: " +
(err instanceof Error ? err.message : "Errore sconosciuto"),
{
duration: 5000,
id: "update-annuncio-all",
},
);
},