1package sql
2
3import (
4 "context"
5 "time"
6
7 "gorm.io/gorm"
8
9 "git.sr.ht/~gabrielgio/img/pkg/database/repository"
10 "git.sr.ht/~gabrielgio/img/pkg/list"
11)
12
13type (
14 Media struct {
15 gorm.Model
16 Name string `gorm:"not null"`
17 Path string `gorm:"not null;unique"`
18 PathHash string `gorm:"not null;unique"`
19 MIMEType string `gorm:"not null"`
20 }
21
22 MediaEXIF struct {
23 gorm.Model
24 Width *float64
25 Height *float64
26 MediaID uint `gorm:"not null"`
27 Media Media
28 Description *string
29 Camera *string
30 Maker *string
31 Lens *string
32 DateShot *time.Time
33 Exposure *float64
34 Aperture *float64
35 Iso *int64
36 FocalLength *float64
37 Flash *int64
38 Orientation *int64
39 ExposureProgram *int64
40 GPSLatitude *float64
41 GPSLongitude *float64
42 }
43
44 MediaThumbnail struct {
45 gorm.Model
46 Path string
47 MediaID uint `gorm:"not null"`
48 Media Media
49 }
50
51 MediaAlbum struct {
52 gorm.Model
53 ParentID *uint
54 Parent *MediaAlbum
55 Name string
56 Path string `gorm:"not null; unique"`
57 }
58
59 MediaAlbumFile struct {
60 gorm.Model
61 MediaID uint
62 Media Media
63 AlbumID uint
64 Album MediaAlbum
65 }
66
67 MediaRepository struct {
68 db *gorm.DB
69 }
70)
71
72var _ repository.MediaRepository = &MediaRepository{}
73
74func (m *Media) ToModel() *repository.Media {
75 return &repository.Media{
76 ID: m.ID,
77 Path: m.Path,
78 PathHash: m.PathHash,
79 Name: m.Name,
80 MIMEType: m.MIMEType,
81 }
82}
83
84func (m *MediaEXIF) ToModel() *repository.MediaEXIF {
85 return &repository.MediaEXIF{
86 Height: m.Height,
87 Width: m.Width,
88 Description: m.Description,
89 Camera: m.Camera,
90 Maker: m.Maker,
91 Lens: m.Lens,
92 DateShot: m.DateShot,
93 Exposure: m.Exposure,
94 Aperture: m.Aperture,
95 Iso: m.Iso,
96 FocalLength: m.FocalLength,
97 Flash: m.Flash,
98 Orientation: m.Orientation,
99 ExposureProgram: m.ExposureProgram,
100 GPSLatitude: m.GPSLatitude,
101 GPSLongitude: m.GPSLongitude,
102 }
103}
104
105func (a *MediaAlbum) ToModel() *repository.Album {
106 return &repository.Album{
107 ID: a.ID,
108 Name: a.Name,
109 Path: a.Path,
110 }
111}
112
113func (m *MediaThumbnail) ToModel() *repository.MediaThumbnail {
114 return &repository.MediaThumbnail{
115 Path: m.Path,
116 }
117}
118
119func NewMediaRepository(db *gorm.DB) *MediaRepository {
120 return &MediaRepository{
121 db: db,
122 }
123}
124
125func (self *MediaRepository) Create(ctx context.Context, createMedia *repository.CreateMedia) error {
126 media := &Media{
127 Name: createMedia.Name,
128 Path: createMedia.Path,
129 PathHash: createMedia.PathHash,
130 MIMEType: createMedia.MIMEType,
131 }
132
133 result := self.db.
134 WithContext(ctx).
135 Create(media)
136 if result.Error != nil {
137 return result.Error
138 }
139
140 return nil
141}
142
143func (self *MediaRepository) Exists(ctx context.Context, path string) (bool, error) {
144 var exists bool
145 result := self.db.
146 WithContext(ctx).
147 Model(&Media{}).
148 Select("count(id) > 0").
149 Where("path_hash = ?", path).
150 Find(&exists)
151
152 if result.Error != nil {
153 return false, result.Error
154 }
155
156 return exists, nil
157}
158
159func (self *MediaRepository) List(ctx context.Context, pagination *repository.Pagination) ([]*repository.Media, error) {
160 medias := make([]*Media, 0)
161 result := self.db.
162 WithContext(ctx).
163 Model(&Media{}).
164 Offset(pagination.Page * pagination.Size).
165 Limit(pagination.Size).
166 Where("path like '" + pagination.Path + "%'").
167 Order("created_at DESC").
168 Find(&medias)
169
170 if result.Error != nil {
171 return nil, result.Error
172 }
173
174 m := list.Map(medias, func(s *Media) *repository.Media {
175 return s.ToModel()
176 })
177
178 return m, nil
179}
180
181func (self *MediaRepository) Get(ctx context.Context, pathHash string) (*repository.Media, error) {
182 m := &Media{}
183 result := self.db.
184 WithContext(ctx).
185 Model(&Media{}).
186 Where("path_hash = ?", pathHash).
187 Limit(1).
188 Take(m)
189
190 if result.Error != nil {
191 return nil, result.Error
192 }
193
194 return m.ToModel(), nil
195}
196
197func (self *MediaRepository) GetPath(ctx context.Context, pathHash string) (string, error) {
198 var path string
199 result := self.db.
200 WithContext(ctx).
201 Model(&Media{}).
202 Select("path").
203 Where("path_hash = ?", pathHash).
204 Limit(1).
205 Find(&path)
206
207 if result.Error != nil {
208 return "", result.Error
209 }
210
211 return path, nil
212}
213
214func (self *MediaRepository) GetThumbnailPath(ctx context.Context, pathHash string) (string, error) {
215 var path string
216 result := self.db.
217 WithContext(ctx).
218 Model(&Media{}).
219 Select("media_thumbnails.path").
220 Joins("left join media_thumbnails on media.id = media_thumbnails.media_id").
221 Where("media.path_hash = ?", pathHash).
222 Limit(1).
223 Find(&path)
224
225 if result.Error != nil {
226 return "", result.Error
227 }
228
229 return path, nil
230}
231
232func (m *MediaRepository) GetEXIF(ctx context.Context, mediaID uint) (*repository.MediaEXIF, error) {
233 exif := &MediaEXIF{}
234 result := m.db.
235 WithContext(ctx).
236 Model(&Media{}).
237 Where("media_id = ?", mediaID).
238 Limit(1).
239 Take(m)
240
241 if result.Error != nil {
242 return nil, result.Error
243 }
244
245 return exif.ToModel(), nil
246}
247
248func (s *MediaRepository) CreateEXIF(ctx context.Context, id uint, info *repository.MediaEXIF) error {
249 media := &MediaEXIF{
250 MediaID: id,
251 Width: info.Width,
252 Height: info.Height,
253 Description: info.Description,
254 Camera: info.Camera,
255 Maker: info.Maker,
256 Lens: info.Lens,
257 DateShot: info.DateShot,
258 Exposure: info.Exposure,
259 Aperture: info.Aperture,
260 Iso: info.Iso,
261 FocalLength: info.FocalLength,
262 Flash: info.Flash,
263 Orientation: info.Orientation,
264 ExposureProgram: info.ExposureProgram,
265 GPSLatitude: info.GPSLatitude,
266 GPSLongitude: info.GPSLongitude,
267 }
268
269 result := s.db.
270 WithContext(ctx).
271 Create(media)
272 if result.Error != nil {
273 return result.Error
274 }
275
276 return nil
277}
278
279func (r *MediaRepository) ListEmptyEXIF(ctx context.Context, pagination *repository.Pagination) ([]*repository.Media, error) {
280 medias := make([]*Media, 0)
281 result := r.db.
282 WithContext(ctx).
283 Model(&Media{}).
284 Joins("left join media_exifs on media.id = media_exifs.media_id").
285 Where("media_exifs.media_id IS NULL AND media.path like '" + pagination.Path + "%'").
286 Offset(pagination.Page * pagination.Size).
287 Limit(pagination.Size).
288 Order("media.created_at DESC").
289 Find(&medias)
290
291 if result.Error != nil {
292 return nil, result.Error
293 }
294
295 m := list.Map(medias, func(s *Media) *repository.Media {
296 return s.ToModel()
297 })
298
299 return m, nil
300}
301
302func (r *MediaRepository) ListEmptyThumbnail(ctx context.Context, pagination *repository.Pagination) ([]*repository.Media, error) {
303 medias := make([]*Media, 0)
304 result := r.db.
305 WithContext(ctx).
306 Model(&Media{}).
307 Joins("left join media_thumbnails on media.id = media_thumbnails.media_id").
308 Where("media_thumbnails.media_id IS NULL AND media.path like '" + pagination.Path + "%'").
309 Offset(pagination.Page * pagination.Size).
310 Limit(pagination.Size).
311 Order("media.created_at DESC").
312 Find(&medias)
313
314 if result.Error != nil {
315 return nil, result.Error
316 }
317
318 m := list.Map(medias, func(s *Media) *repository.Media {
319 return s.ToModel()
320 })
321
322 return m, nil
323}
324
325func (m *MediaRepository) GetThumbnail(ctx context.Context, mediaID uint) (*repository.MediaThumbnail, error) {
326 thumbnail := &MediaThumbnail{}
327 result := m.db.
328 WithContext(ctx).
329 Model(&Media{}).
330 Where("media_id = ?", mediaID).
331 Limit(1).
332 Take(m)
333
334 if result.Error != nil {
335 return nil, result.Error
336 }
337
338 return thumbnail.ToModel(), nil
339}
340
341func (m *MediaRepository) CreateThumbnail(ctx context.Context, mediaID uint, thumbnail *repository.MediaThumbnail) error {
342 media := &MediaThumbnail{
343 MediaID: mediaID,
344 Path: thumbnail.Path,
345 }
346
347 result := m.db.
348 WithContext(ctx).
349 Create(media)
350 if result.Error != nil {
351 return result.Error
352 }
353
354 return nil
355}
356
357func (r *MediaRepository) ListEmptyAlbums(ctx context.Context, pagination *repository.Pagination) ([]*repository.Media, error) {
358 medias := make([]*Media, 0)
359 result := r.db.
360 WithContext(ctx).
361 Model(&Media{}).
362 Joins("left join media_album_files on media.id = media_album_files.media_id").
363 Where("media_album_files.media_id IS NULL").
364 Offset(pagination.Page * pagination.Size).
365 Limit(pagination.Size).
366 Order("media.created_at DESC").
367 Find(&medias)
368
369 if result.Error != nil {
370 return nil, result.Error
371 }
372
373 m := list.Map(medias, func(s *Media) *repository.Media {
374 return s.ToModel()
375 })
376
377 return m, nil
378}
379
380func (m *MediaRepository) ExistsAlbumByAbsolutePath(ctx context.Context, path string) (bool, error) {
381 var exists bool
382 result := m.db.
383 WithContext(ctx).
384 Model(&MediaAlbum{}).
385 Select("count(id) > 0").
386 Where("path = ?", path).
387 Find(&exists)
388
389 if result.Error != nil {
390 return false, result.Error
391 }
392
393 return exists, nil
394}
395
396func (r *MediaRepository) GetAlbumByAbsolutePath(ctx context.Context, path string) (*repository.Album, error) {
397 m := &MediaAlbum{}
398 result := r.db.
399 WithContext(ctx).
400 Model(&MediaAlbum{}).
401 Where("path = ?", path).
402 Limit(1).
403 Take(m)
404
405 if result.Error != nil {
406 return nil, result.Error
407 }
408
409 return m.ToModel(), nil
410}
411
412func (r *MediaRepository) GetAlbum(ctx context.Context, albumID uint) (*repository.Album, error) {
413 m := &MediaAlbum{}
414 result := r.db.
415 WithContext(ctx).
416 Model(&MediaAlbum{}).
417 Where("id = ?", albumID).
418 Limit(1).
419 Take(m)
420
421 if result.Error != nil {
422 return nil, result.Error
423 }
424
425 return m.ToModel(), nil
426}
427
428func (m *MediaRepository) CreateAlbum(ctx context.Context, createAlbum *repository.CreateAlbum) (*repository.Album, error) {
429 album := &MediaAlbum{
430 ParentID: createAlbum.ParentID,
431 Name: createAlbum.Name,
432 Path: createAlbum.Path,
433 }
434
435 result := m.db.
436 WithContext(ctx).
437 Create(album)
438 if result.Error != nil {
439 return nil, result.Error
440 }
441
442 return album.ToModel(), nil
443}
444
445func (m *MediaRepository) CreateAlbumFile(ctx context.Context, createAlbumFile *repository.CreateAlbumFile) error {
446 albumFile := &MediaAlbumFile{
447 MediaID: createAlbumFile.MediaID,
448 AlbumID: createAlbumFile.AlbumID,
449 }
450
451 result := m.db.
452 WithContext(ctx).
453 Create(albumFile)
454 if result.Error != nil {
455 return result.Error
456 }
457
458 return nil
459}
460
461func (m *MediaRepository) ListAlbums(ctx context.Context, albumID uint) ([]*repository.Album, error) {
462 albums := make([]*MediaAlbum, 0)
463
464 result := m.db.
465 WithContext(ctx).
466 Model(&MediaAlbum{}).
467 Where("parent_id = ?", albumID).
468 Find(&albums)
469
470 if result.Error != nil {
471 return nil, result.Error
472 }
473
474 return list.Map(albums, func(a *MediaAlbum) *repository.Album {
475 return a.ToModel()
476 }), nil
477}