all repos — weimar @ 30c984bbd9a52787565d44cc121088cf1ee25029

Unnamed repository; edit this file 'description' to name the repository.

cmd/weimar/image.go (view raw)

  1package main
  2
  3import (
  4	"fmt"
  5	"strconv"
  6	"strings"
  7
  8	"codeberg.org/maxwelljensen/weimar/internal/config"
  9	"codeberg.org/maxwelljensen/weimar/internal/db"
 10	"codeberg.org/maxwelljensen/weimar/internal/storage"
 11	"github.com/spf13/cobra"
 12)
 13
 14var imageCmd = &cobra.Command{
 15	Use:   "image",
 16	Short: "Manage images",
 17}
 18
 19var imageDeleteCmd = &cobra.Command{
 20	Use:   "delete <id>",
 21	Short: "Delete an image by ID",
 22	Args:  cobra.ExactArgs(1),
 23	RunE: func(cmd *cobra.Command, args []string) error {
 24		id, err := strconv.ParseInt(args[0], 10, 64)
 25		if err != nil {
 26			return fmt.Errorf("invalid image ID: %w", err)
 27		}
 28
 29		cfg := config.FromViper()
 30		database, err := db.Open(cfg.Database.Path)
 31		if err != nil {
 32			return fmt.Errorf("opening database: %w", err)
 33		}
 34		defer database.Close()
 35
 36		img, err := database.GetImageByID(cmd.Context(), id)
 37		if err != nil {
 38			return fmt.Errorf("looking up image: %w", err)
 39		}
 40
 41		st, err := storage.New(cfg.Storage.Path)
 42		if err != nil {
 43			return fmt.Errorf("opening storage: %w", err)
 44		}
 45
 46		// Delete thumbnail first if it exists.
 47		if img.ThumbnailPath != nil {
 48			if err := st.Delete(*img.ThumbnailPath); err != nil {
 49				return fmt.Errorf("deleting thumbnail: %w", err)
 50			}
 51		}
 52
 53		// Delete original file.
 54		if err := st.Delete(img.StoragePath); err != nil {
 55			return fmt.Errorf("deleting file: %w", err)
 56		}
 57
 58		// Delete database record.
 59		if err := database.DeleteImage(cmd.Context(), id); err != nil {
 60			return fmt.Errorf("deleting image record: %w", err)
 61		}
 62
 63		fmt.Fprintf(cmd.OutOrStdout(), "deleted image %d: %s\n", img.ID, img.Filename)
 64		return nil
 65	},
 66}
 67
 68var imageRescanInnateCmd = &cobra.Command{
 69	Use:   "rescan-innate",
 70	Short: "Recompute innate tags and backfill video metadata for all images",
 71	Long: `Scans every image in the database and:
 72- Computes innate tags (image, video, gif, lowres, highres, >10 sec, <10 sec)
 73  for any record that has no innate tags yet.
 74- Probes and stores width, height, and duration for any video that has 0x0
 75  dimensions (backfill for uploads made before video probing was added).
 76
 77This is useful after upgrading from an older version that did not support
 78innate tags or video dimension probing.`,
 79	Args: cobra.NoArgs,
 80	RunE: func(cmd *cobra.Command, args []string) error {
 81		cfg := config.FromViper()
 82
 83		database, err := db.Open(cfg.Database.Path)
 84		if err != nil {
 85			return fmt.Errorf("opening database: %w", err)
 86		}
 87		defer database.Close()
 88
 89		st, err := storage.New(cfg.Storage.Path)
 90		if err != nil {
 91			return fmt.Errorf("opening storage: %w", err)
 92		}
 93
 94		images, err := database.ListAllImages(cmd.Context())
 95		if err != nil {
 96			return fmt.Errorf("listing images: %w", err)
 97		}
 98
 99		var innateUpdated, videoUpdated int
100		for _, img := range images {
101			isVideo := storage.IsVideo(img.MimeType)
102
103			// Step 1: probe and fix video dimensions/duration if needed.
104			if isVideo && (img.Width == 0 || img.Height == 0 || img.Duration == 0) {
105				videoAbs := st.AbsPath(img.StoragePath)
106				w, h := storage.GetVideoDimensions(videoAbs)
107				dur := storage.GetVideoDuration(videoAbs)
108				if err := database.UpdateImageVideoMetadata(cmd.Context(), img.ID, w, h, dur); err != nil {
109					fmt.Fprintf(cmd.ErrOrStderr(), "error updating video metadata for image %d: %v\n", img.ID, err)
110					continue
111				}
112				img.Width, img.Height, img.Duration = w, h, dur
113				videoUpdated++
114				fmt.Fprintf(cmd.OutOrStdout(), "  video %d (%s): %dx%d, %.1fs\n", img.ID, img.Filename, w, h, dur)
115			}
116
117			// Step 2: compute innate tags if missing.
118			if len(img.InnateTags) > 0 {
119				continue
120			}
121
122			videoPath := ""
123			if isVideo {
124				videoPath = st.AbsPath(img.StoragePath)
125			}
126			tags := storage.ComputeInnateTags(img.MimeType, img.Width, img.Height, videoPath)
127
128			if err := database.UpdateImageInnateTags(cmd.Context(), img.ID, tags); err != nil {
129				fmt.Fprintf(cmd.ErrOrStderr(), "error updating image %d: %v\n", img.ID, err)
130				continue
131			}
132			innateUpdated++
133			fmt.Fprintf(cmd.OutOrStdout(), "  image %d (%s): %v\n", img.ID, img.Filename, tags)
134		}
135
136		var parts []string
137		if videoUpdated > 0 {
138			parts = append(parts, fmt.Sprintf("updated %d video(s)", videoUpdated))
139		}
140		if innateUpdated > 0 {
141			parts = append(parts, fmt.Sprintf("updated %d image(s) with innate tags", innateUpdated))
142		}
143		if len(parts) == 0 {
144			fmt.Fprintf(cmd.OutOrStdout(), "nothing to update\n")
145		} else {
146			fmt.Fprintf(cmd.OutOrStdout(), "done — %s\n", strings.Join(parts, ", "))
147		}
148		return nil
149	},
150}
151
152func init() {
153	rootCmd.AddCommand(imageCmd)
154	imageCmd.AddCommand(imageDeleteCmd)
155	imageCmd.AddCommand(imageRescanInnateCmd)
156}