package main import ( "fmt" "strconv" "strings" "codeberg.org/maxwelljensen/weimar/internal/config" "codeberg.org/maxwelljensen/weimar/internal/db" "codeberg.org/maxwelljensen/weimar/internal/storage" "github.com/spf13/cobra" ) var imageCmd = &cobra.Command{ Use: "image", Short: "Manage images", } var imageDeleteCmd = &cobra.Command{ Use: "delete ", Short: "Delete an image by ID", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { id, err := strconv.ParseInt(args[0], 10, 64) if err != nil { return fmt.Errorf("invalid image ID: %w", err) } cfg := config.FromViper() database, err := db.Open(cfg.Database.Path) if err != nil { return fmt.Errorf("opening database: %w", err) } defer database.Close() img, err := database.GetImageByID(cmd.Context(), id) if err != nil { return fmt.Errorf("looking up image: %w", err) } st, err := storage.New(cfg.Storage.Path) if err != nil { return fmt.Errorf("opening storage: %w", err) } // Delete thumbnail first if it exists. if img.ThumbnailPath != nil { if err := st.Delete(*img.ThumbnailPath); err != nil { return fmt.Errorf("deleting thumbnail: %w", err) } } // Delete original file. if err := st.Delete(img.StoragePath); err != nil { return fmt.Errorf("deleting file: %w", err) } // Delete database record. if err := database.DeleteImage(cmd.Context(), id); err != nil { return fmt.Errorf("deleting image record: %w", err) } fmt.Fprintf(cmd.OutOrStdout(), "deleted image %d: %s\n", img.ID, img.Filename) return nil }, } var imageRescanInnateCmd = &cobra.Command{ Use: "rescan-innate", Short: "Recompute innate tags and backfill video metadata for all images", Long: `Scans every image in the database and: - Computes innate tags (image, video, gif, lowres, highres, >10 sec, <10 sec) for any record that has no innate tags yet. - Probes and stores width, height, and duration for any video that has 0x0 dimensions (backfill for uploads made before video probing was added). This is useful after upgrading from an older version that did not support innate tags or video dimension probing.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { cfg := config.FromViper() database, err := db.Open(cfg.Database.Path) if err != nil { return fmt.Errorf("opening database: %w", err) } defer database.Close() st, err := storage.New(cfg.Storage.Path) if err != nil { return fmt.Errorf("opening storage: %w", err) } images, err := database.ListAllImages(cmd.Context()) if err != nil { return fmt.Errorf("listing images: %w", err) } var innateUpdated, videoUpdated int for _, img := range images { isVideo := storage.IsVideo(img.MimeType) // Step 1: probe and fix video dimensions/duration if needed. if isVideo && (img.Width == 0 || img.Height == 0 || img.Duration == 0) { videoAbs := st.AbsPath(img.StoragePath) w, h := storage.GetVideoDimensions(videoAbs) dur := storage.GetVideoDuration(videoAbs) if err := database.UpdateImageVideoMetadata(cmd.Context(), img.ID, w, h, dur); err != nil { fmt.Fprintf(cmd.ErrOrStderr(), "error updating video metadata for image %d: %v\n", img.ID, err) continue } img.Width, img.Height, img.Duration = w, h, dur videoUpdated++ fmt.Fprintf(cmd.OutOrStdout(), " video %d (%s): %dx%d, %.1fs\n", img.ID, img.Filename, w, h, dur) } // Step 2: compute innate tags if missing. if len(img.InnateTags) > 0 { continue } videoPath := "" if isVideo { videoPath = st.AbsPath(img.StoragePath) } tags := storage.ComputeInnateTags(img.MimeType, img.Width, img.Height, videoPath) if err := database.UpdateImageInnateTags(cmd.Context(), img.ID, tags); err != nil { fmt.Fprintf(cmd.ErrOrStderr(), "error updating image %d: %v\n", img.ID, err) continue } innateUpdated++ fmt.Fprintf(cmd.OutOrStdout(), " image %d (%s): %v\n", img.ID, img.Filename, tags) } var parts []string if videoUpdated > 0 { parts = append(parts, fmt.Sprintf("updated %d video(s)", videoUpdated)) } if innateUpdated > 0 { parts = append(parts, fmt.Sprintf("updated %d image(s) with innate tags", innateUpdated)) } if len(parts) == 0 { fmt.Fprintf(cmd.OutOrStdout(), "nothing to update\n") } else { fmt.Fprintf(cmd.OutOrStdout(), "done — %s\n", strings.Join(parts, ", ")) } return nil }, } func init() { rootCmd.AddCommand(imageCmd) imageCmd.AddCommand(imageDeleteCmd) imageCmd.AddCommand(imageRescanInnateCmd) }