diff --git a/apps/backend/.air.toml b/apps/backend/.air.toml index 8d61370..400b463 100644 --- a/apps/backend/.air.toml +++ b/apps/backend/.air.toml @@ -7,7 +7,7 @@ tmp_dir = "tmp" bin = "tmp\\main.exe" cmd = "go build -o ./tmp/main.exe ." delay = 1000 - exclude_dir = ["assets", "tmp", "vendor", "testdata", "images", ".bin"] + exclude_dir = ["assets", "tmp", "vendor", "testdata", "images", ".bin", "videos"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile index f33fcf1..fd7dee7 100644 --- a/apps/backend/Dockerfile +++ b/apps/backend/Dockerfile @@ -14,10 +14,10 @@ RUN go mod download COPY . . # Build the Go app -RUN go build -o main . +RUN CGO_ENABLED=0 GOOS=linux go build -o main . # Start a new stage from alpine:latest -FROM alpine:3.22 AS runner +FROM alpine:latest AS runner ARG POSTGRES_USER ARG POSTGRES_PASSWORD @@ -29,7 +29,7 @@ ARG CONCURRENT_IMAGES ARG STORAGE_URL ARG STORAGE_TOKEN -RUN apk --no-cache add ca-certificates libc6-compat +RUN apk add --no-cache ffmpeg ca-certificates libc6-compat ENV GOMEMLIMIT=2750MiB ENV GOGC=100 ENV PGHOST=$PGHOST @@ -52,8 +52,9 @@ COPY --from=builder /app/scripts /app/scripts RUN chmod +x /app/scripts/*.sh #COPY --from=builder /app/.env /app/.env -# create images +# create folders RUN mkdir images +RUN mkdir videos RUN apk add curl diff --git a/apps/backend/config/config.go b/apps/backend/config/config.go index c2c352f..1fe4e1b 100644 --- a/apps/backend/config/config.go +++ b/apps/backend/config/config.go @@ -3,11 +3,13 @@ package config import ( "fmt" "os" + "os/exec" "path/filepath" "strconv" "sync" "github.com/joho/godotenv" + "github.com/labstack/gommon/log" ) // Config holds all application configuration @@ -26,6 +28,11 @@ type Config struct { ConcurrentLimit int Path string } + Videos struct { + Enabled bool + ConcurrentLimit int + Path string + } Storage struct { Endpoint string Token string @@ -67,6 +74,11 @@ func Load() *Config { Cfg.Images.ConcurrentLimit = getEnvAsInt("CONCURRENT_IMAGES", 5) Cfg.Images.Path = filepath.Join(pwd, "images") + // Video processing config + Cfg.Videos.Enabled = getEnvAsBool("VIDEOOPTION", true) + Cfg.Videos.ConcurrentLimit = getEnvAsInt("CONCURRENT_VIDEOS", 2) + Cfg.Videos.Path = filepath.Join(pwd, "videos") + // Storage config Cfg.Storage.Endpoint = getEnv("STORAGE_URL", "") Cfg.Storage.Token = getEnv("STORAGE_TOKEN", "") @@ -80,6 +92,11 @@ func Load() *Config { } }) + if err := exec.Command("ffmpeg", "-version").Run(); err != nil { + log.Errorf("FFmpeg not found: %w", err) + Cfg.Videos.Enabled = false + } + return &Cfg } diff --git a/apps/backend/from_bkp.go b/apps/backend/from_bkp.go index 0384da1..ffa2191 100644 --- a/apps/backend/from_bkp.go +++ b/apps/backend/from_bkp.go @@ -188,7 +188,7 @@ func extractData_bkp(annuncio typesdefs.AnnuncioBKP, extractedData chan typesdef w := false tmp.Web = &w - tmp.Url_video = nil + tmp.External_videos = nil if annuncio.Accessori != nil && annuncio.Accessori.Accessorio != nil { tmpAcc := make([]string, 0) @@ -230,7 +230,7 @@ func extractData_bkp(annuncio typesdefs.AnnuncioBKP, extractedData chan typesdef } tmp.Caratteristiche = &tmpCaratteristiche - tmp.Url_video = nil + tmp.External_videos = nil extractedData <- tmp diff --git a/apps/backend/images.go b/apps/backend/images.go index c95bbe3..716fb9d 100644 --- a/apps/backend/images.go +++ b/apps/backend/images.go @@ -14,21 +14,22 @@ import ( "net/http" "os" "path/filepath" - "slices" + "strings" "sync" + "time" "github.com/labstack/echo/v4" "github.com/nfnt/resize" "github.com/nickalie/go-webpbin" ) -func AddToImageProcessing(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.AnnuncioXML, p *[]typesdefs.ImagesToProcess) { +func AddToImageProcessing(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.AnnuncioXML, p *[]typesdefs.MediaToProcess) { if annuncio.Foto == nil || len(*annuncio.Foto) == 0 { return } for idx, url := range *annuncio.Foto { - *p = append(*p, typesdefs.ImagesToProcess{ + *p = append(*p, typesdefs.MediaToProcess{ Codice: tmp.Codice, Url: url, Order: idx, @@ -36,10 +37,10 @@ func AddToImageProcessing(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.Ann } } -func ProcessImagePool(toProcess *[]typesdefs.ImagesToProcess) ([]typesdefs.UploadedImage, error) { +func ProcessImagePool(toProcess *[]typesdefs.MediaToProcess) ([]typesdefs.UploadedMedia, error) { var ( - ProcessedImages []typesdefs.ProcessedImage - UploadedImages []typesdefs.UploadedImage + ProcessedImages []typesdefs.ProcessedMedia + UploadedImages []typesdefs.UploadedMedia mu sync.Mutex wg sync.WaitGroup ) @@ -55,17 +56,21 @@ func ProcessImagePool(toProcess *[]typesdefs.ImagesToProcess) ([]typesdefs.Uploa for _, img := range *toProcess { sem <- struct{}{} // acquire semaphore wg.Add(1) - go func(img typesdefs.ImagesToProcess) { + go func(img typesdefs.MediaToProcess) { defer wg.Done() defer func() { <-sem }() // release semaphore imgPath := filepath.Join(config.Cfg.Images.Path, fmt.Sprintf("%s_%d%s", img.Codice, img.Order, filepath.Ext(img.Url))) - downloadImage(img.Url, imgPath) + err := downloadImage(img.Url, imgPath) + if err != nil { + log.Printf("Failed to download image %s: %v", img.Url, err) + return // Exit goroutine on download failure + } webpPath, thumbnailwebpPath, err := Conversion(imgPath) if err != nil { log.Fatalf("Failed to convert image: %v", err) } mu.Lock() - ProcessedImages = append(ProcessedImages, typesdefs.ProcessedImage{ + ProcessedImages = append(ProcessedImages, typesdefs.ProcessedMedia{ Codice: img.Codice, Order: img.Order, Path: webpPath, @@ -81,25 +86,25 @@ func ProcessImagePool(toProcess *[]typesdefs.ImagesToProcess) ([]typesdefs.Uploa for _, img := range ProcessedImages { uploadSem <- struct{}{} // acquire semaphore uploadWg.Add(1) - go func(img typesdefs.ProcessedImage) { + go func(img typesdefs.ProcessedMedia) { defer uploadWg.Done() defer func() { <-uploadSem }() // release semaphore - imgId, err := UploadFile(img.Path) + imgId, err := UploadFile(img.Path, "image/webp", "images") if err != nil { log.Printf("failed to upload image: %v", err) return } - thumbId, err := UploadFile(img.ThumnailPath) + thumbId, err := UploadFile(img.ThumnailPath, "image/webp", "images") if err != nil { log.Printf("failed to upload thumbnail: %v", err) return } mu.Lock() - UploadedImages = append(UploadedImages, typesdefs.UploadedImage{ + UploadedImages = append(UploadedImages, typesdefs.UploadedMedia{ Codice: img.Codice, Order: img.Order, - ImgId: imgId, + MediaId: imgId, ThumbId: thumbId, OGUrl: img.OGUrl, }) @@ -123,21 +128,38 @@ func clearImageFolder() error { return nil } func downloadImage(url, filename string) error { - response, err := http.Get(url) + client := &http.Client{ + Timeout: 2 * time.Minute, // Adjust based on expected image sizes + } + + response, err := client.Get(url) if err != nil { - return fmt.Errorf("failed to download image: %v", err) + return fmt.Errorf("failed to download image: %w", err) } defer response.Body.Close() + + // Check HTTP status + if response.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download image: HTTP %d", response.StatusCode) + } + + // Optional: Validate Content-Type + contentType := response.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + log.Printf("Warning: unexpected content type %s for %s", contentType, url) + } + file, err := os.Create(filename) if err != nil { - - return fmt.Errorf("failed to create image file: %v", err) + return fmt.Errorf("failed to create image file: %w", err) } defer file.Close() + _, err = io.Copy(file, response.Body) if err != nil { - return fmt.Errorf("failed to save image file: %v", err) + return fmt.Errorf("failed to save image file: %w", err) } + return nil } @@ -257,9 +279,9 @@ func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) { return nil, fmt.Errorf("failed to list bucket: %w", err) } //make a list of bucket ids - bucketIds := []string{} + bucketIdsMap := make(map[string]bool) for _, item := range currentBucketState { - bucketIds = append(bucketIds, item.ID) + bucketIdsMap[item.ID] = true } db_images, err := queries.GetImagesFromDB() @@ -290,12 +312,9 @@ func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) { found := false for _, ref := range refFoto { if fotoUrl == ref.Og_url { - if slices.Contains(bucketIds, ref.Img) && slices.Contains(bucketIds, ref.Thumb) { + if bucketIdsMap[ref.Img] && bucketIdsMap[ref.Thumb] { found = true break - } else { - needProcess = true - break } } } @@ -303,9 +322,6 @@ func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) { needProcess = true break } - if needProcess { - break - } } if needProcess { codiciToProcess = append(codiciToProcess, *annuncio.Codice) @@ -342,13 +358,3 @@ func ForceClearImages(codici []string) error { } return nil } - -func DeleteIdsFromStorage(ids []string) error { - for _, id := range ids { - err := DeleteFile(id) - if err != nil { - return fmt.Errorf("failed to delete file with id %s: %w", id, err) - } - } - return nil -} diff --git a/apps/backend/miogest_handler.go b/apps/backend/miogest_handler.go index 5ea97b2..6d3590c 100644 --- a/apps/backend/miogest_handler.go +++ b/apps/backend/miogest_handler.go @@ -90,15 +90,15 @@ func (m *MiogestHandler) GenerateCategorie() { } // ANNUNCI PARSED -func (m *MiogestHandler) GetAnnunciParsed(p *[]typesdefs.ImagesToProcess, forceImages bool) typesdefs.AnnunciParsed { +func (m *MiogestHandler) GetAnnunciParsed(imgPool *[]typesdefs.MediaToProcess, videoPool *[]typesdefs.MediaToProcess, forceMediaStale bool) typesdefs.AnnunciParsed { // salvo il parsing nello struct nel caso voglio implementare un caching e non fare il parsing ad ogni chiamata - m.AnnunciParsed = m.ParseAnnunci("", p, forceImages) + m.AnnunciParsed = m.ParseAnnunci("", imgPool, videoPool, forceMediaStale) return m.AnnunciParsed } // versione per singolo annuncio -func (m *MiogestHandler) GetAnnuncioParsed(codFilter string, p *[]typesdefs.ImagesToProcess) typesdefs.AnnunciParsed { - data := m.ParseAnnunci(codFilter, p, false) +func (m *MiogestHandler) GetAnnuncioParsed(codFilter string, imgPool *[]typesdefs.MediaToProcess, videoPool *[]typesdefs.MediaToProcess) typesdefs.AnnunciParsed { + data := m.ParseAnnunci(codFilter, imgPool, videoPool, false) if len(data.Annuncio) == 0 { return typesdefs.AnnunciParsed{} } @@ -106,7 +106,7 @@ func (m *MiogestHandler) GetAnnuncioParsed(codFilter string, p *[]typesdefs.Imag } -func (m *MiogestHandler) ParseAnnunci(codFilter string, p *[]typesdefs.ImagesToProcess, forceImages bool) typesdefs.AnnunciParsed { +func (m *MiogestHandler) ParseAnnunci(codFilter string, imgPool *[]typesdefs.MediaToProcess, videoPool *[]typesdefs.MediaToProcess, forceMediaStale bool) typesdefs.AnnunciParsed { annunci := m.GetAnnunci() if len(annunci.Annuncio) == 0 { @@ -126,7 +126,7 @@ func (m *MiogestHandler) ParseAnnunci(codFilter string, p *[]typesdefs.ImagesToP image_codes_to_process := []string{} if config.Cfg.Images.Enabled { var err error - if forceImages { + if forceMediaStale { // process all images image_codes_to_process = make([]string, 0, len(annunci.Annuncio)) for _, annuncio := range annunci.Annuncio { @@ -152,6 +152,38 @@ func (m *MiogestHandler) ParseAnnunci(codFilter string, p *[]typesdefs.ImagesToP log.Fatalf("Failed to clear images for codice %s: %v", codFilter, err.Error()) } + } + video_codes_to_process := []string{} + if config.Cfg.Videos.Enabled { + var err error + if forceMediaStale { + // process all videos + video_codes_to_process = make([]string, 0, len(annunci.Annuncio)) + for _, annuncio := range annunci.Annuncio { + if annuncio.Codice != nil { + video_codes_to_process = append(video_codes_to_process, *annuncio.Codice) + } + } + + } else { + // process only new or updated videos + video_codes_to_process, err = CodiciToProcessVideos(&annunci) + if err != nil { + log.Fatalf("Failed to find videos to update: %v", err.Error()) + } + + } + if codFilter != "" { + // if processing single codice, ensure it's in the list + if !slices.Contains(video_codes_to_process, codFilter) { + video_codes_to_process = append(video_codes_to_process, codFilter) + } + } + err = ForceClearVideos(video_codes_to_process) + if err != nil { + log.Fatalf("Failed to clear videos for codice %s: %v", codFilter, err.Error()) + } + } var AnnunciArray typesdefs.AnnunciParsed @@ -160,7 +192,8 @@ func (m *MiogestHandler) ParseAnnunci(codFilter string, p *[]typesdefs.ImagesToP for i := range annunci.Annuncio { imgToUpdate := slices.Contains(image_codes_to_process, *annunci.Annuncio[i].Codice) - result := extractData_update(&annunci.Annuncio[i], imgToUpdate, m, p) + updateVideos := slices.Contains(video_codes_to_process, *annunci.Annuncio[i].Codice) + result := extractData_update(&annunci.Annuncio[i], imgToUpdate, updateVideos, m, imgPool, videoPool) AnnunciArray.Annuncio = append(AnnunciArray.Annuncio, result) } @@ -178,7 +211,7 @@ func strUpd(s string) *string { return &s } -func extractData_update(annuncio *typesdefs.AnnuncioXML, updateImages bool, m *MiogestHandler, p *[]typesdefs.ImagesToProcess) typesdefs.AnnuncioParsed { +func extractData_update(annuncio *typesdefs.AnnuncioXML, updateImages bool, updateVideos bool, m *MiogestHandler, imgPool *[]typesdefs.MediaToProcess, videoPool *[]typesdefs.MediaToProcess) typesdefs.AnnuncioParsed { var tmp typesdefs.AnnuncioParsed = typesdefs.AnnuncioParsed{} tmp.Codice = utils.RemoveWhitespace(utils.GetStringValue(annuncio.Codice)) processLocatore(&tmp, annuncio) @@ -231,20 +264,21 @@ func extractData_update(annuncio *typesdefs.AnnuncioXML, updateImages bool, m *M tmp.Web = boolUpd(true) - videos := []string{} + if updateVideos { + AddToVideoProcessing(&tmp, annuncio, videoPool) + } + if annuncio.Video != nil { - videos = *annuncio.Video - } - miogestVideos := []string{} - if annuncio.AMMedias != nil { - for _, media := range *annuncio.AMMedias { - if media.AMVideo != nil && media.AMVideo.Url != nil { - miogestVideos = append(miogestVideos, *media.AMVideo.Url) + ext_videos := []string{} + for _, v := range *annuncio.Video { + if strings.Contains(v, "youtu") { + ext_videos = append(ext_videos, v) } + tmp.External_videos = &ext_videos } + } else { + tmp.External_videos = &[]string{} } - concatenatedVideos := slices.Concat(videos, miogestVideos) - tmp.Url_video = &concatenatedVideos if annuncio.Accessori != nil { tmp.Accessori = annuncio.Accessori @@ -253,7 +287,7 @@ func extractData_update(annuncio *typesdefs.AnnuncioXML, updateImages bool, m *M tmp.Caratteristiche = &tmpcaratt if updateImages { - AddToImageProcessing(&tmp, annuncio, p) + AddToImageProcessing(&tmp, annuncio, imgPool) } return tmp diff --git a/apps/backend/queries/annunci.go b/apps/backend/queries/annunci.go index 69f9b8c..215bfb7 100644 --- a/apps/backend/queries/annunci.go +++ b/apps/backend/queries/annunci.go @@ -84,7 +84,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e stato = $42, web = $43, homepage = $44, - url_video = $45, + external_videos = $45, disponibile_da = $47, permanenza = $48, persone = $49, @@ -101,7 +101,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e annuncio.Numero_balconi, annuncio.Numero_terrazzi, annuncio.Numero_box, annuncio.Numero_postiauto, annuncio.Caratteristiche, annuncio.Accessori, annuncio.Titolo_it, annuncio.Desc_it, annuncio.Titolo_en, annuncio.Desc_en, annuncio.Stato, annuncio.Web, annuncio.Homepage, - annuncio.Url_video, annuncio.Codice, annuncio.Disponibile_da, annuncio.Permanenza, annuncio.Persone) + annuncio.External_videos, annuncio.Codice, annuncio.Disponibile_da, annuncio.Permanenza, annuncio.Persone) } else { batch.Queue(` @@ -150,7 +150,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e stato, web, homepage, - url_video, + external_videos, codice, disponibile_da, permanenza, @@ -169,7 +169,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e annuncio.Numero_balconi, annuncio.Numero_terrazzi, annuncio.Numero_box, annuncio.Numero_postiauto, annuncio.Caratteristiche, annuncio.Accessori, annuncio.Titolo_it, annuncio.Desc_it, annuncio.Titolo_en, annuncio.Desc_en, annuncio.Stato, annuncio.Web, annuncio.Homepage, - annuncio.Url_video, annuncio.Codice, annuncio.Disponibile_da, annuncio.Permanenza, annuncio.Persone) + annuncio.External_videos, annuncio.Codice, annuncio.Disponibile_da, annuncio.Permanenza, annuncio.Persone) } } err = Db.Dbpool.SendBatch(Db.Ctx, batch).Close() @@ -241,7 +241,7 @@ func SetFromBkp(annunci typesdefs.AnnunciParsed) error { stato, web, homepage, - url_video, + external_videos, codice ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46) @@ -256,7 +256,7 @@ func SetFromBkp(annunci typesdefs.AnnunciParsed) error { annuncio.Numero_balconi, annuncio.Numero_terrazzi, annuncio.Numero_box, annuncio.Numero_postiauto, annuncio.Caratteristiche, annuncio.Accessori, annuncio.Titolo_it, annuncio.Desc_it, annuncio.Titolo_en, annuncio.Desc_en, annuncio.Stato, annuncio.Web, annuncio.Homepage, - annuncio.Url_video, annuncio.Codice) + annuncio.External_videos, annuncio.Codice) } err := Db.Dbpool.SendBatch(Db.Ctx, batch).Close() @@ -266,11 +266,3 @@ func SetFromBkp(annunci typesdefs.AnnunciParsed) error { fmt.Println("Annunci insert/update done") return nil } - -func AnnunciResetImages() error { - _, err := Db.Dbpool.Exec(Db.Ctx, "UPDATE public.annunci SET url_immagini = NULL, url_video = NULL") - if err != nil { - return fmt.Errorf("failed to reset images: %w", err) - } - return nil -} diff --git a/apps/backend/queries/images.go b/apps/backend/queries/images.go index 08446f8..b15f428 100644 --- a/apps/backend/queries/images.go +++ b/apps/backend/queries/images.go @@ -8,7 +8,7 @@ import ( "github.com/jackc/pgx/v5" ) -func SetImagesToDB(pool *[]typesdefs.UploadedImage) error { +func SetImagesToDB(pool *[]typesdefs.UploadedMedia) error { batch := &pgx.Batch{} for _, data := range *pool { @@ -16,7 +16,7 @@ func SetImagesToDB(pool *[]typesdefs.UploadedImage) error { INSERT INTO public.images_refs (codice, ordine, img, thumb, og_url) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (codice, ordine) DO UPDATE SET img = $3, thumb = $4 - `, data.Codice, data.Order, data.ImgId, data.ThumbId, data.OGUrl) + `, data.Codice, data.Order, data.MediaId, data.ThumbId, data.OGUrl) } if err := Db.Dbpool.SendBatch(Db.Ctx, batch).Close(); err != nil { diff --git a/apps/backend/queries/videos.go b/apps/backend/queries/videos.go new file mode 100644 index 0000000..d5d89b3 --- /dev/null +++ b/apps/backend/queries/videos.go @@ -0,0 +1,53 @@ +package queries + +import ( + "backend/typesdefs" + "fmt" + + "github.com/georgysavva/scany/v2/pgxscan" + "github.com/jackc/pgx/v5" +) + +func SetVideosToDB(pool *[]typesdefs.UploadedMedia) error { + batch := &pgx.Batch{} + + for _, data := range *pool { + batch.Queue(` + INSERT INTO public.videos_refs (codice, ordine, video, thumb, og_url) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (codice, ordine) DO UPDATE SET video = $3, thumb = $4 + `, data.Codice, data.Order, data.MediaId, data.ThumbId, data.OGUrl) + } + + if err := Db.Dbpool.SendBatch(Db.Ctx, batch).Close(); err != nil { + return fmt.Errorf("failed to insert/update video batch: %w", err) + } + + return nil +} + +func GetVideosFromDB() ([]typesdefs.VideosRefs, error) { + var videos []typesdefs.VideosRefs + err := pgxscan.Select(Db.Ctx, Db.Dbpool, &videos, "SELECT * FROM public.videos_refs ORDER BY ordine ASC") + if err != nil && err != pgx.ErrNoRows { + return nil, fmt.Errorf("failed to get videos: %w", err) + } + return videos, nil +} + +func GetCodiceVideosFromDB(codice string) ([]typesdefs.VideosRefs, error) { + var videos []typesdefs.VideosRefs + err := pgxscan.Select(Db.Ctx, Db.Dbpool, &videos, "SELECT * FROM public.videos_refs WHERE codice = $1 ORDER BY ordine ASC", codice) + if err != nil && err != pgx.ErrNoRows { + return nil, fmt.Errorf("failed to get videos: %w", err) + } + return videos, nil +} + +func ClearVideosRefsByCodice(codice string) error { + _, err := Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.videos_refs WHERE codice = $1", codice) + if err != nil { + return fmt.Errorf("failed to clear videos refs by codice: %w", err) + } + return nil +} diff --git a/apps/backend/server.go b/apps/backend/server.go index 4d90c2e..867a3b4 100644 --- a/apps/backend/server.go +++ b/apps/backend/server.go @@ -104,43 +104,60 @@ func (s *Server) SetupRoutes() *echo.Echo { }) e.GET("/parse", func(c echo.Context) error { start := time.Now() - var data = s.miogest.GetAnnunciParsed(nil, false) + var data = s.miogest.GetAnnunciParsed(nil, nil, false) end := time.Now() fmt.Println("time taken:", end.Sub(start).String()) return c.JSONPretty(http.StatusOK, data, " ") }) e.GET("/update", func(c echo.Context) error { - forceImages := false - forceImagesStr := c.QueryParam("forceimages") - if forceImagesStr == "true" { - forceImages = true + forceMediaStale := false + forceMediaStr := c.QueryParam("forcemedia") + if forceMediaStr == "true" { + forceMediaStale = true } - imgPool := []typesdefs.ImagesToProcess{} - var data = s.miogest.GetAnnunciParsed(&imgPool, forceImages) + imgPool := []typesdefs.MediaToProcess{} + videoPool := []typesdefs.MediaToProcess{} + + var data = s.miogest.GetAnnunciParsed(&imgPool, &videoPool, forceMediaStale) + imgRefs, err := ProcessImagePool(&imgPool) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) } + videoRefs, err := ProcessVideoPool(&videoPool) + if err != nil { + return c.String(http.StatusInternalServerError, err.Error()) + } err = queries.AnnunciInsert(data, true) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) } + err = queries.SetImagesToDB(&imgRefs) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) } + err = queries.SetVideosToDB(&videoRefs) + if err != nil { + return c.String(http.StatusInternalServerError, err.Error()) + } return c.String(http.StatusOK, "Updated") }) e.GET("/update-cod/:cod", func(c echo.Context) error { cod := c.Param("cod") - imgPool := []typesdefs.ImagesToProcess{} - var data = s.miogest.GetAnnuncioParsed(cod, &imgPool) + imgPool := []typesdefs.MediaToProcess{} + videoPool := []typesdefs.MediaToProcess{} + var data = s.miogest.GetAnnuncioParsed(cod, &imgPool, &videoPool) imgRefs, err := ProcessImagePool(&imgPool) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) } + videoRefs, err := ProcessVideoPool(&videoPool) + if err != nil { + return c.String(http.StatusInternalServerError, err.Error()) + } err = queries.AnnunciInsert(data, false) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) @@ -149,6 +166,10 @@ func (s *Server) SetupRoutes() *echo.Echo { if err != nil { return c.String(http.StatusInternalServerError, err.Error()) } + err = queries.SetVideosToDB(&videoRefs) + if err != nil { + return c.String(http.StatusInternalServerError, err.Error()) + } return c.String(http.StatusOK, "Updated") }) diff --git a/apps/backend/storage.go b/apps/backend/storage.go index d3cfa52..3490e35 100644 --- a/apps/backend/storage.go +++ b/apps/backend/storage.go @@ -30,6 +30,16 @@ func DeleteFile(id string) error { return nil } +func DeleteIdsFromStorage(ids []string) error { + for _, id := range ids { + err := DeleteFile(id) + if err != nil { + return fmt.Errorf("failed to delete file with id %s: %w", id, err) + } + } + return nil +} + func GetFile(path string) ([]byte, error) { panic("unimplemented") } @@ -136,7 +146,7 @@ func ListBucket(bucketName string) ([]FileMetadata, error) { // } -func UploadFile(path string) (string, error) { +func UploadFile(path string, mimeType string, bucket string) (string, error) { file, err := os.Open(path) if err != nil { @@ -147,8 +157,6 @@ func UploadFile(path string) (string, error) { var body bytes.Buffer writer := multipart.NewWriter(&body) - mimeType := "image/webp" - h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, filepath.Base(path))) h.Set("Content-Type", mimeType) @@ -160,7 +168,7 @@ func UploadFile(path string) (string, error) { return "", fmt.Errorf("failed to copy file data: %w", err) } - err = writer.WriteField("bucket", "images") + err = writer.WriteField("bucket", bucket) if err != nil { return "", fmt.Errorf("failed to write bucket field: %w", err) } diff --git a/apps/backend/typesdefs/models.go b/apps/backend/typesdefs/models.go index cc043a6..e67f468 100644 --- a/apps/backend/typesdefs/models.go +++ b/apps/backend/typesdefs/models.go @@ -257,7 +257,7 @@ type AnnuncioParsed struct { Titolo_en *string Unita_condominio *string Numero_vani *string - Url_video *[]string + External_videos *[]string Web *bool Disponibile_da *string Permanenza *[]int @@ -308,7 +308,7 @@ type AnnunciDB struct { Web *bool Caratteristiche *map[string]interface{} Homepage *bool - Url_video *[]string + External_videos *[]string Email *string Creato_il *time.Time Modificato_il *time.Time @@ -358,13 +358,21 @@ type ImagesRefs struct { Og_url string } -type ImagesToProcess struct { +type VideosRefs struct { + Codice string + Ordine int + Video string + Thumb string + Og_url string +} + +type MediaToProcess struct { Codice string Url string Order int } -type ProcessedImage struct { +type ProcessedMedia struct { Codice string Order int Path string @@ -372,10 +380,10 @@ type ProcessedImage struct { OGUrl string } -type UploadedImage struct { +type UploadedMedia struct { Codice string Order int - ImgId string + MediaId string ThumbId string OGUrl string } diff --git a/apps/backend/video.go b/apps/backend/video.go new file mode 100644 index 0000000..dd4d78e --- /dev/null +++ b/apps/backend/video.go @@ -0,0 +1,399 @@ +package main + +import ( + "backend/config" + "backend/queries" + "backend/typesdefs" + "backend/utils" + "fmt" + "image/jpeg" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/labstack/echo/v4" + "github.com/nfnt/resize" + "github.com/nickalie/go-webpbin" +) + +func AddToVideoProcessing(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.AnnuncioXML, p *[]typesdefs.MediaToProcess) { + + miogestVideos := []string{} + if annuncio.AMMedias != nil { + for _, media := range *annuncio.AMMedias { + if media.AMVideo != nil && media.AMVideo.Url != nil { + if strings.Contains(*media.AMVideo.Url, "video.miogest") { + miogestVideos = append(miogestVideos, *media.AMVideo.Url) + } + + } + } + } + + for idx, url := range miogestVideos { + *p = append(*p, typesdefs.MediaToProcess{ + Codice: tmp.Codice, + Url: url, + Order: idx, + }) + } +} + +func ProcessVideoPool(toProcess *[]typesdefs.MediaToProcess) ([]typesdefs.UploadedMedia, error) { + var ( + ProcessedVideos []typesdefs.ProcessedMedia + UploadedVideos []typesdefs.UploadedMedia + mu sync.Mutex + wg sync.WaitGroup + ) + sem := make(chan struct{}, config.Cfg.Videos.ConcurrentLimit) + + // clear videos folder + err := clearVideoFolder() + if err != nil { + log.Fatalf("Failed to initialize image processing: %v", err) + } + + // process videos + for _, video := range *toProcess { + sem <- struct{}{} // acquire semaphore + wg.Add(1) + go func(video typesdefs.MediaToProcess) { + defer wg.Done() + defer func() { <-sem }() // release semaphore + videoPath := filepath.Join(config.Cfg.Videos.Path, fmt.Sprintf("%s_%d%s", video.Codice, video.Order, filepath.Ext(video.Url))) + err := downloadVideo(video.Url, videoPath) + if err != nil { + log.Printf("Failed to download video %s: %v", video.Url, err) + return // Exit goroutine on download failure + } + // Transcode and generate thumbnail + mp4Path, thumbnailPath, err := VideoConversion(videoPath) + if err != nil { + log.Printf("Failed to convert video %s: %v", videoPath, err) + return + } + mu.Lock() + ProcessedVideos = append(ProcessedVideos, typesdefs.ProcessedMedia{ + Codice: video.Codice, + Order: video.Order, + Path: mp4Path, + ThumnailPath: thumbnailPath, + OGUrl: video.Url, + }) + mu.Unlock() + }(video) + } + wg.Wait() + uploadSem := make(chan struct{}, config.Cfg.Videos.ConcurrentLimit) + var uploadWg sync.WaitGroup + for _, video := range ProcessedVideos { + uploadSem <- struct{}{} // acquire semaphore + uploadWg.Add(1) + go func(video typesdefs.ProcessedMedia) { + defer uploadWg.Done() + defer func() { <-uploadSem }() // release semaphore + + videoId, err := UploadFile(video.Path, "video/mp4", "videos") + if err != nil { + log.Printf("failed to upload video: %v", err) + return + } + thumbId, err := UploadFile(video.ThumnailPath, "image/webp", "videos") + if err != nil { + log.Printf("failed to upload thumbnail: %v", err) + return + } + mu.Lock() + UploadedVideos = append(UploadedVideos, typesdefs.UploadedMedia{ + Codice: video.Codice, + Order: video.Order, + MediaId: videoId, + ThumbId: thumbId, + OGUrl: video.OGUrl, + }) + mu.Unlock() + }(video) + } + uploadWg.Wait() + + return UploadedVideos, nil +} + +func clearVideoFolder() error { + err := utils.SafeRemoveAll(config.Cfg.Videos.Path) + if err != nil { + return err + } + err = os.Mkdir(config.Cfg.Videos.Path, 0755) + if err != nil { + return err + } + return nil +} + +func downloadVideo(url, filename string) error { + client := &http.Client{ + Timeout: 5 * time.Minute, // Adjust based on expected video sizes + } + + response, err := client.Get(url) + if err != nil { + return fmt.Errorf("failed to download video: %w", err) + } + defer response.Body.Close() + + // Check HTTP status + if response.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download video: HTTP %d", response.StatusCode) + } + + // Optional: Validate Content-Type + contentType := response.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "video/") && !strings.HasPrefix(contentType, "application/") { + log.Printf("Warning: unexpected content type %s for %s", contentType, url) + } + + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to create video file: %w", err) + } + defer file.Close() + + _, err = io.Copy(file, response.Body) + if err != nil { + return fmt.Errorf("failed to save video file: %w", err) + } + + return nil +} + +// VideoConversion transcodes video and generates thumbnail +func VideoConversion(input string) (string, string, error) { + outputdir := filepath.Dir(input) + basename := filepath.Base(input) + ext := filepath.Ext(basename) + nameWithoutExt := basename[0 : len(basename)-len(ext)] + + tempVideo := filepath.Join(outputdir, nameWithoutExt+"_temp.mp4") + 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) + 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) + if err != nil { + return "", "", fmt.Errorf("failed to extract thumbnail: %w", err) + } + os.Remove(input) + err = os.Rename(tempVideo, videoOutput) + if err != nil { + return "", "", fmt.Errorf("failed to rename converted video: %w", err) + } + + return videoOutput, thumbOutput, nil +} + +// transcodeVideo converts video to web-optimized MP4 +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", + "-preset", "fast", + "-crf", "23", + "-c:a", "aac", + "-b:a", "128k", + "-movflags", "+faststart", + "-vf", "scale=min(1280\\,iw):-2", // Escape comma for cross-platform + "-y", + output, + ) + + output_bytes, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("ffmpeg transcode failed: %w, output: %s", err, string(output_bytes)) + } + + return nil +} + +// extractThumbnail generates a WebP thumbnail from video +func extractThumbnail(input, output string) error { + // Extract frame at 1 second as JPEG to temp file + tempJpeg := output + ".tmp.jpg" + defer os.Remove(tempJpeg) + + cmd := exec.Command("ffmpeg", + "-i", input, + "-ss", "00:00:01", // seek to 1 second + "-vframes", "1", // extract 1 frame + "-q:v", "2", // JPEG quality (2-31, lower = better) + "-y", + tempJpeg, + ) + + output_bytes, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("ffmpeg thumbnail extraction failed: %w, output: %s", err, string(output_bytes)) + } + + // Convert JPEG to WebP thumbnail + f, err := os.Open(tempJpeg) + if err != nil { + return fmt.Errorf("failed to open temp jpeg: %w", err) + } + defer f.Close() + + img, err := jpeg.Decode(f) + if err != nil { + return fmt.Errorf("failed to decode jpeg: %w", err) + } + + // Resize to thumbnail (320px width) + resizedImg := resize.Resize(320, 0, img, resize.Lanczos3) + + out, err := os.Create(output) + if err != nil { + return fmt.Errorf("failed to create thumbnail: %w", err) + } + defer out.Close() + + err = webpbin.Encode(out, resizedImg) + if err != nil { + return fmt.Errorf("failed to encode webp: %w", err) + } + + return nil +} + +func FallbackVideo(c echo.Context) error { + // Serve a fallback image if the requested image is not found + fallbackPath := "fallback.webp" // Path to your fallback image + if _, err := os.Stat(fallbackPath); os.IsNotExist(err) { + return c.String(http.StatusNotFound, "Fallback image not found") + } + return c.File(fallbackPath) +} + +type videRef struct { + Og_url string + Video string + Thumb string +} + +func CodiciToProcessVideos(annunci *typesdefs.AnnunciXML) ([]string, error) { + currentBucketState, err := ListBucket("videos") + if err != nil { + return nil, fmt.Errorf("failed to list bucket: %w", err) + } + //make a list of bucket ids + bucketIdsMap := make(map[string]bool) + for _, item := range currentBucketState { + bucketIdsMap[item.ID] = true + } + + db_videos, err := queries.GetVideosFromDB() + if err != nil { + return nil, err + } + + dbVideosMap := make(map[string][]videRef) // Convert slice to map for faster lookup + for _, ref := range db_videos { + dbVideosMap[ref.Codice] = append(dbVideosMap[ref.Codice], videRef{ + Og_url: ref.Og_url, + Video: ref.Video, + Thumb: ref.Thumb, + }) + } + + var codiciToProcess []string + for _, annuncio := range annunci.Annuncio { + + if annuncio.Codice != nil && annuncio.AMMedias != nil { + + refVideos, existsInDb := dbVideosMap[*annuncio.Codice] + if !existsInDb { + codiciToProcess = append(codiciToProcess, *annuncio.Codice) + continue + } + needProcess := false + + for _, media := range *annuncio.AMMedias { + if media.AMVideo == nil || media.AMVideo.Url == nil { + continue + } + if !strings.Contains(*media.AMVideo.Url, "video.miogest") { + continue + } + + found := false + for _, ref := range refVideos { + if *media.AMVideo.Url == ref.Og_url { + if bucketIdsMap[ref.Video] && bucketIdsMap[ref.Thumb] { + found = true + break + } + } + } + if !found { + needProcess = true + break + } + + } + if needProcess { + codiciToProcess = append(codiciToProcess, *annuncio.Codice) + } + } + } + + return codiciToProcess, nil + +} + +func ForceClearVideos(codici []string) error { + var idsToDelete []string + for _, codice := range codici { + refs, err := queries.GetCodiceVideosFromDB(codice) + if err != nil { + return fmt.Errorf("failed to get videos by codice %s: %w", codice, err) + } + for _, ref := range refs { + idsToDelete = append(idsToDelete, ref.Video) + idsToDelete = append(idsToDelete, ref.Thumb) + } + + } + err := DeleteIdsFromStorage(idsToDelete) + if err != nil { + return fmt.Errorf("failed to delete videos from storage: %w", err) + } + for _, codice := range codici { + err := queries.ClearVideosRefsByCodice(codice) + if err != nil { + return fmt.Errorf("failed to clear videos by codice %s: %w", codice, err) + } + } + return nil +} diff --git a/apps/db/migrations/26_videos_refs.up.sql b/apps/db/migrations/26_videos_refs.up.sql new file mode 100644 index 0000000..2275aea --- /dev/null +++ b/apps/db/migrations/26_videos_refs.up.sql @@ -0,0 +1,59 @@ +-- Images_refs index +CREATE INDEX IF NOT EXISTS annunci_imgs ON images_refs (codice); + +-- Videos_refs Table +CREATE TABLE IF NOT EXISTS public.videos_refs ( + codice TEXT COLLATE pg_catalog."default" NOT NULL, + ordine integer NOT NULL, + video TEXT COLLATE pg_catalog."default" NOT NULL, + thumb TEXT COLLATE pg_catalog."default" NOT NULL, + og_url TEXT COLLATE pg_catalog."default" NOT NULL +); + +-- Unique Codice and Ordine for Videos_refs Table +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'video_cod_order' + AND conrelid = 'public.videos_refs'::regclass + ) THEN + ALTER TABLE public.videos_refs + ADD CONSTRAINT video_cod_order UNIQUE (codice, ordine); + END IF; +END $$; + +-- Foreign Key to Annunci for Videos_refs Table +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'cod_video' + AND conrelid = 'public.videos_refs'::regclass + ) THEN + ALTER TABLE public.videos_refs + ADD CONSTRAINT cod_video + FOREIGN KEY (codice) REFERENCES public.annunci (codice) + ON UPDATE CASCADE ON DELETE CASCADE; + END IF; +END $$; + +-- Index for Videos_refs Table +CREATE INDEX IF NOT EXISTS annunci_videos ON videos_refs (codice); + +-- Rename url_video to external_videos in Annunci Table +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name='annunci' + AND column_name='url_video' + ) THEN + ALTER TABLE public.annunci + RENAME COLUMN url_video TO external_videos; + +END IF; + +END $$; \ No newline at end of file diff --git a/apps/infoalloggi/src/components/annuncio_card.tsx b/apps/infoalloggi/src/components/annuncio_card.tsx index 23fd450..ead2235 100644 --- a/apps/infoalloggi/src/components/annuncio_card.tsx +++ b/apps/infoalloggi/src/components/annuncio_card.tsx @@ -29,8 +29,9 @@ import { import { camereTesti, handleConsegna } from "~/lib/annuncio_details"; import { cn, formatCurrency } from "~/lib/utils"; import { useTranslation } from "~/providers/I18nProvider"; +import type { ImagesRefs } from "~/schemas/public/ImagesRefs"; +import type { VideosRefs } from "~/schemas/public/VideosRefs"; import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller"; - import { Badge } from "./ui/badge"; import { Skeleton } from "./ui/skeleton"; import { VideoPlayer } from "./videoPlayer"; @@ -125,7 +126,7 @@ export const CardAnnuncio = ({ tipo, stato, className, - url_video: videos, + videos, media_updated_at, images, homepage, @@ -226,10 +227,6 @@ export const CardAnnuncio = ({ ))} {videos?.map((video) => { - if (!video) return null; - if (video.includes("youtu")) { - return null; - } return ( ); @@ -392,8 +385,8 @@ export const CarouselAnnuncio = ({ updated_at, }: { single?: boolean; - immagini: string[] | null; - videos: string[] | null; + immagini: Pick[]; + videos: Pick[]; updated_at: Date | null; }) => { const [openModal, setOpenModal] = useState(false); @@ -416,10 +409,10 @@ export const CarouselAnnuncio = ({ ? "" : !single && "md:basis-1/2 lg:basis-1/3", )} - key={`${img}-cF`} + key={`${img.img}-cF`} > handleOpenModal(idx)} priority={idx === 0} - src={`/storage-api/get/${img}?image=true&${updated_at?.toString() || new Date().toString()}`} + src={`/storage-api/get/${img.img}?image=true&${updated_at?.toString() || new Date().toString()}`} width={800} /> ))} {videos?.map((video, idx) => { - if (!video) return null; - if (video.includes("youtu")) { - return null; - } return ( {immagini?.map((img, idx) => ( - + ))} {videos?.map((video) => { - if (!video) return null; - if (video.includes("youtu")) { - return null; - } return ( ); diff --git a/apps/infoalloggi/src/components/servizio/annuncio_dettaglio.tsx b/apps/infoalloggi/src/components/servizio/annuncio_dettaglio.tsx index be47e2d..894108e 100644 --- a/apps/infoalloggi/src/components/servizio/annuncio_dettaglio.tsx +++ b/apps/infoalloggi/src/components/servizio/annuncio_dettaglio.tsx @@ -59,9 +59,9 @@ export const AnnuncioDettaglio = ({ data }: { data: AnnuncioRicerca }) => {
img.img)} + immagini={data.images} updated_at={data.media_updated_at} - videos={data.url_video} + videos={data.videos} />
diff --git a/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx b/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx index cea967e..16f6d4a 100644 --- a/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx +++ b/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx @@ -46,12 +46,12 @@ import { cn } from "~/lib/utils"; import { useZodForm } from "~/lib/zodForm"; import { StatusBadge } from "~/pages/area-riservata/admin/edit-annuncio/[id]"; import { useTranslation } from "~/providers/I18nProvider"; -import type { AnnunciWithImages } from "~/server/controllers/annunci.controller"; +import type { AnnunciWithMedia } from "~/server/controllers/annunci.controller"; import { zAnnuncioId } from "~/server/utils/zod_types"; import { api } from "~/utils/api"; import type { Caratteristiche } from "~/utils/kanel-types"; -export const AnnuncioEditForm = ({ data }: { data: AnnunciWithImages }) => { +export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => { const schema = z.object({ accessori: z.string().array().nullable(), @@ -516,10 +516,10 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithImages }) => { v.img) || "fallback"} + immagini={data.images} single updated_at={data.media_updated_at} - videos={data.url_video} + videos={data.videos} /> @@ -1050,7 +1050,6 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithImages }) => { "h-[42px] w-full pl-3 text-left font-medium", !field.value && "text-muted-foreground", "rounded-lg border border-neutral-300 shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm disabled:cursor-not-allowed disabled:opacity-50 dark:focus:border-transparent", - )} id="disponibile_da" variant={"outline"} diff --git a/apps/infoalloggi/src/pages/annunci.tsx b/apps/infoalloggi/src/pages/annunci.tsx index 8b0d45c..f7f39e6 100644 --- a/apps/infoalloggi/src/pages/annunci.tsx +++ b/apps/infoalloggi/src/pages/annunci.tsx @@ -434,6 +434,7 @@ const AnnunciList = () => { key={annuncio.codice} {...annuncio} eager={idx <= 9} + videos={[]} /> ))}
diff --git a/apps/infoalloggi/src/pages/annuncio/[cod].tsx b/apps/infoalloggi/src/pages/annuncio/[cod].tsx index 2d52320..5d49dcd 100644 --- a/apps/infoalloggi/src/pages/annuncio/[cod].tsx +++ b/apps/infoalloggi/src/pages/annuncio/[cod].tsx @@ -60,6 +60,7 @@ import { useTranslation } from "~/providers/I18nProvider"; import { useSession } from "~/providers/SessionProvider"; import type { Annunci } from "~/schemas/public/Annunci"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; +import type { VideosRefs } from "~/schemas/public/VideosRefs"; import { generateSSGHelper } from "~/server/utils/ssgHelper"; import { api } from "~/utils/api"; @@ -135,9 +136,9 @@ const AnnuncioView = ({ cod, flag }: Omit) => {
img.img)} + immagini={data.images} updated_at={data.media_updated_at} - videos={data.url_video} + videos={data.videos} />
@@ -200,14 +201,10 @@ const AnnuncioView = ({ cod, flag }: Omit) => {
@@ -615,33 +612,45 @@ const AnnuncioFooter = ({ tipo, comune, consegna, - url_video, - first_image, + videos, + external_videos, updated_at, }: { tipo: string | null; comune: string | null; consegna: number | null; - url_video: string[]; - first_image: string; + videos: Pick[]; + external_videos: string[]; updated_at: Date | null; }) => { return (

Video:

- {url_video.map((video) => { - if (!video) return null; - if (video.includes("youtu")) { - return null; - } + {videos.map((video) => { return ( + ); + })} + {external_videos.map((videoUrl) => { + const embedUrl = getYouTubeEmbedUrl(videoUrl); + if (!embedUrl) return null; + return ( +