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()) return c.String(http.StatusInternalServerError, err.Error())
} }
*/ */
log.Println("Update completed")
return c.String(http.StatusOK, "Updated") return c.String(http.StatusOK, "Updated")
}) })
e.GET("/update-cod/:cod", func(c echo.Context) error { 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()) return c.String(http.StatusInternalServerError, err.Error())
} }
*/ */
log.Printf("Update %s completed", cod)
return c.String(http.StatusOK, "Updated") return c.String(http.StatusOK, "Updated")
}) })

View file

@ -197,42 +197,65 @@ func VideoConversion(input string) (string, string, error) {
ext := filepath.Ext(basename) ext := filepath.Ext(basename)
nameWithoutExt := basename[0 : len(basename)-len(ext)] 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") videoOutput := filepath.Join(outputdir, nameWithoutExt+".mp4")
thumbOutput := filepath.Join(outputdir, "thumbnail-"+nameWithoutExt+".webp") thumbOutput := filepath.Join(outputdir, "thumbnail-"+nameWithoutExt+".webp")
// 1. Transcode video to H.264/AAC MP4 (web-optimized) // Use /tmp for all ffmpeg work: /tmp is tmpfs and supports random-access
//log.Println("Transcoding video:", input) // seeks that overlayfs does not, which is required for MP4 muxing and
err := transcodeVideo(input, tempVideo) // 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 { if err != nil {
return "", "", fmt.Errorf("failed to transcode video: %w", err) return "", "", fmt.Errorf("failed to transcode video: %w", err)
} }
// 2. Extract thumbnail from video at 1 second // 2. Extract thumbnail from original video into /tmp
//log.Println("Extracting thumbnail from:", input) err = extractThumbnail(input, tmpThumb)
err = extractThumbnail(input, thumbOutput)
if err != nil { if err != nil {
return "", "", fmt.Errorf("failed to extract thumbnail: %w", err) return "", "", fmt.Errorf("failed to extract thumbnail: %w", err)
} }
// 3. Move completed files from /tmp to the videos volume
os.Remove(input) os.Remove(input)
err = os.Rename(tempVideo, videoOutput) if err = copyFile(tmpVideo, videoOutput); err != nil {
if err != nil { return "", "", fmt.Errorf("failed to move converted video: %w", err)
return "", "", fmt.Errorf("failed to rename converted video: %w", err) }
if err = copyFile(tmpThumb, thumbOutput); err != nil {
return "", "", fmt.Errorf("failed to move thumbnail: %w", err)
} }
return videoOutput, thumbOutput, nil 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 { 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", cmd := exec.Command("ffmpeg",
"-i", input, "-i", input,
"-c:v", "libx264", "-c:v", "libx264",
@ -241,7 +264,7 @@ func transcodeVideo(input, output string) error {
"-c:a", "aac", "-c:a", "aac",
"-b:a", "128k", "-b:a", "128k",
"-movflags", "+faststart", "-movflags", "+faststart",
"-vf", "scale=min(1280\\,iw):-2", // Escape comma for cross-platform "-vf", "scale=min(1280\\,iw):-2",
"-y", "-y",
output, output,
) )

View file

@ -141,15 +141,26 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => {
}); });
const { mutateAsync: update } = api.revalidation.updateAnnuncio.useMutation({ const { mutateAsync: update } = api.revalidation.updateAnnuncio.useMutation({
onMutate: () => {
toast.loading("Richiesta di aggiornamento inviata, attendere prego...", {
id: "update-annuncio",
});
},
onSettled: async () => { onSettled: async () => {
await utils.annunci.getAnnuncio.invalidate({ cod: data.codice }); await utils.annunci.getAnnuncio.invalidate({ cod: data.codice });
await utils.annunci.getAnnuncioById_rawImgUrls.invalidate({ await utils.annunci.getAnnuncioById_rawImgUrls.invalidate({
id: data.id, id: data.id,
}); });
toast.success( toast.success("Aggiornamento completato", {
"L'aggiornamento dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.", 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 utils = api.useUtils();
const { mutateAsync: update } = api.revalidation.updateAllAnnunci.useMutation( const { mutateAsync: update } = api.revalidation.updateAllAnnunci.useMutation(
{ {
onMutate: () => {
toast.loading(
"Richiesta di aggiornamento inviata, attendere prego...",
{
id: "update-annuncio-all",
},
);
},
onSettled: async () => { onSettled: async () => {
await utils.annunci.getAnnunciList.invalidate(); await utils.annunci.getAnnunciList.invalidate();
toast.success( toast.success("Aggiornamento completato", {
"La revalidazione dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.", 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",
}, },
); );
}, },