internal/api/images.go (view raw)
1package api
2
3import (
4 "encoding/json"
5 "net/http"
6 "strconv"
7
8 "codeberg.org/maxwelljensen/weimar/internal/db"
9)
10
11func (a *API) HandleListImages(w http.ResponseWriter, r *http.Request) {
12 tags := r.URL.Query()["tags"]
13 uploader := r.URL.Query().Get("uploader")
14 sortGems := r.URL.Query().Get("sort") == "gems"
15 filterGems := r.URL.Query().Get("gems") == "true"
16
17 page, _ := strconv.Atoi(r.URL.Query().Get("page"))
18 if page < 1 {
19 page = 1
20 }
21 perPage, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
22 if perPage < 1 || perPage > 100 {
23 perPage = 20
24 }
25 offset := (page - 1) * perPage
26
27 result, err := a.DB.ListImages(r.Context(), tags, uploader, perPage, offset, sortGems, filterGems)
28 if err != nil {
29 writeError(w, http.StatusInternalServerError, "failed to list images")
30 return
31 }
32
33 // Load tags and uploader for each image.
34 for i := range result.Images {
35 img := &result.Images[i]
36
37 tags, err := a.DB.GetImageTags(r.Context(), img.ID)
38 if err == nil {
39 img.Tags = tags
40 }
41
42 user, err := a.DB.GetUserByID(r.Context(), img.UploadedBy)
43 if err == nil {
44 img.UploaderUsername = user.Username
45 if user.AvatarPath != nil {
46 img.AvatarURL = "/api/users/" + strconv.FormatInt(img.UploadedBy, 10) + "/avatar"
47 }
48 }
49 }
50
51 writeJSON(w, http.StatusOK, map[string]any{
52 "images": result.Images,
53 "total": result.Total,
54 "page": page,
55 "per_page": perPage,
56 })
57}
58
59func (a *API) HandleGetImage(w http.ResponseWriter, r *http.Request) {
60 idStr := r.PathValue("id")
61 id, err := strconv.ParseInt(idStr, 10, 64)
62 if err != nil {
63 writeError(w, http.StatusBadRequest, "invalid image id")
64 return
65 }
66
67 img, err := a.DB.GetImageByID(r.Context(), id)
68 if err != nil {
69 writeError(w, http.StatusNotFound, "image not found")
70 return
71 }
72
73 tagsDetail, err := a.DB.GetImageTagsWithCounts(r.Context(), id)
74 if err != nil {
75 tagsDetail = make([]db.Tag, 0)
76 }
77
78 uploaderUsername := ""
79 avatarURL := ""
80 user, err := a.DB.GetUserByID(r.Context(), img.UploadedBy)
81 if err == nil {
82 uploaderUsername = user.Username
83 if user.AvatarPath != nil {
84 avatarURL = "/api/users/" + strconv.FormatInt(img.UploadedBy, 10) + "/avatar"
85 }
86 }
87
88 authUser := UserFromContext(r.Context())
89 isOwner := authUser != nil && authUser.ID == img.UploadedBy
90
91 // Check gem status for authenticated users.
92 gemmed := false
93 if authUser != nil {
94 gemmed, _ = a.DB.GetUserGemStatus(r.Context(), id, authUser.ID)
95 }
96
97 writeJSON(w, http.StatusOK, map[string]any{
98 "id": img.ID,
99 "filename": img.Filename,
100 "storage_path": img.StoragePath,
101 "thumbnail_path": img.ThumbnailPath,
102 "uploaded_by": img.UploadedBy,
103 "uploader_username": uploaderUsername,
104 "avatar_url": avatarURL,
105 "is_owner": isOwner,
106 "file_size": img.FileSize,
107 "width": img.Width,
108 "height": img.Height,
109 "mime_type": img.MimeType,
110 "innate_tags": img.InnateTags,
111 "duration": img.Duration,
112 "gem_count": img.GemCount,
113 "gemmed": gemmed,
114 "created_at": img.CreatedAt,
115 "updated_at": img.UpdatedAt,
116 "tags": img.Tags,
117 "tags_detail": tagsDetail,
118 })
119}
120
121// HandleToggleGem toggles the gem status for an image by the current user.
122// Returns {"gemmed": bool, "gem_count": int}.
123func (a *API) HandleToggleGem(w http.ResponseWriter, r *http.Request) {
124 user := UserFromContext(r.Context())
125 if user == nil {
126 writeError(w, http.StatusUnauthorized, "unauthorized")
127 return
128 }
129
130 idStr := r.PathValue("id")
131 id, err := strconv.ParseInt(idStr, 10, 64)
132 if err != nil {
133 writeError(w, http.StatusBadRequest, "invalid image id")
134 return
135 }
136
137 // Verify image exists.
138 if _, err := a.DB.GetImageByID(r.Context(), id); err != nil {
139 writeError(w, http.StatusNotFound, "image not found")
140 return
141 }
142
143 gemmed, gemCount, err := a.DB.ToggleGem(r.Context(), id, user.ID)
144 if err != nil {
145 writeError(w, http.StatusInternalServerError, "failed to toggle gem")
146 return
147 }
148
149 writeJSON(w, http.StatusOK, map[string]any{
150 "gemmed": gemmed,
151 "gem_count": gemCount,
152 })
153}
154
155type tagsRequest struct {
156 Tags []string `json:"tags"`
157}
158
159func (a *API) HandleAddTags(w http.ResponseWriter, r *http.Request) {
160 user := UserFromContext(r.Context())
161 if user == nil {
162 writeError(w, http.StatusUnauthorized, "unauthorized")
163 return
164 }
165
166 idStr := r.PathValue("id")
167 id, err := strconv.ParseInt(idStr, 10, 64)
168 if err != nil {
169 writeError(w, http.StatusBadRequest, "invalid image id")
170 return
171 }
172
173 img, err := a.DB.GetImageByID(r.Context(), id)
174 if err != nil {
175 writeError(w, http.StatusNotFound, "image not found")
176 return
177 }
178 if img.UploadedBy != user.ID {
179 writeError(w, http.StatusForbidden, "you can only edit tags on your own uploads")
180 return
181 }
182
183 var req tagsRequest
184 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
185 writeError(w, http.StatusBadRequest, "invalid JSON body")
186 return
187 }
188
189 if len(req.Tags) == 0 {
190 writeJSON(w, http.StatusOK, map[string]any{"tags_detail": img.Tags})
191 return
192 }
193
194 if err := a.DB.AddImageTags(r.Context(), id, req.Tags); err != nil {
195 writeError(w, http.StatusInternalServerError, "failed to add tags")
196 return
197 }
198
199 tagsDetail, err := a.DB.GetImageTagsWithCounts(r.Context(), id)
200 if err != nil {
201 tagsDetail = make([]db.Tag, 0)
202 }
203
204 writeJSON(w, http.StatusOK, map[string]any{"tags_detail": tagsDetail})
205}
206
207func (a *API) HandleRemoveTags(w http.ResponseWriter, r *http.Request) {
208 user := UserFromContext(r.Context())
209 if user == nil {
210 writeError(w, http.StatusUnauthorized, "unauthorized")
211 return
212 }
213
214 idStr := r.PathValue("id")
215 id, err := strconv.ParseInt(idStr, 10, 64)
216 if err != nil {
217 writeError(w, http.StatusBadRequest, "invalid image id")
218 return
219 }
220
221 img, err := a.DB.GetImageByID(r.Context(), id)
222 if err != nil {
223 writeError(w, http.StatusNotFound, "image not found")
224 return
225 }
226 if img.UploadedBy != user.ID {
227 writeError(w, http.StatusForbidden, "you can only edit tags on your own uploads")
228 return
229 }
230
231 var req tagsRequest
232 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
233 writeError(w, http.StatusBadRequest, "invalid JSON body")
234 return
235 }
236
237 if len(req.Tags) == 0 {
238 writeJSON(w, http.StatusOK, map[string]any{"tags_detail": img.Tags})
239 return
240 }
241
242 if err := a.DB.RemoveImageTags(r.Context(), id, req.Tags); err != nil {
243 writeError(w, http.StatusInternalServerError, "failed to remove tags")
244 return
245 }
246
247 tagsDetail, err := a.DB.GetImageTagsWithCounts(r.Context(), id)
248 if err != nil {
249 tagsDetail = make([]db.Tag, 0)
250 }
251
252 writeJSON(w, http.StatusOK, map[string]any{"tags_detail": tagsDetail})
253}