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}