Initial implementation
This commit is contained in:
167
internal/storage/photos.go
Normal file
167
internal/storage/photos.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/luxick/chronological/internal/models"
|
||||
)
|
||||
|
||||
// PhotoStore handles scanning and serving photos from the filesystem.
|
||||
type PhotoStore struct {
|
||||
rootFolder string
|
||||
}
|
||||
|
||||
// NewPhotoStore creates a new photo store with the given root folder.
|
||||
func NewPhotoStore(rootFolder string) *PhotoStore {
|
||||
return &PhotoStore{
|
||||
rootFolder: rootFolder,
|
||||
}
|
||||
}
|
||||
|
||||
// Supported image extensions
|
||||
var supportedExtensions = map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".webp": true,
|
||||
}
|
||||
|
||||
// Filename pattern: YYYY-MM-DD_description.ext
|
||||
var filenamePattern = regexp.MustCompile(`^(\d{4})-(\d{2})-(\d{2})_(.+)\.(\w+)$`)
|
||||
|
||||
// LoadAllPhotos scans and loads all photos from the configured directory.
|
||||
func (s *PhotoStore) LoadAllPhotos() ([]*models.Photo, error) {
|
||||
if s.rootFolder == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var photos []*models.Photo
|
||||
|
||||
// Walk through year folders
|
||||
entries, err := os.ReadDir(s.rootFolder)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read photo root folder: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
yearFolder := entry.Name()
|
||||
yearPath := filepath.Join(s.rootFolder, yearFolder)
|
||||
|
||||
yearPhotos, err := s.loadPhotosFromYear(yearFolder, yearPath)
|
||||
if err != nil {
|
||||
continue // Skip problematic folders
|
||||
}
|
||||
|
||||
photos = append(photos, yearPhotos...)
|
||||
}
|
||||
|
||||
return photos, nil
|
||||
}
|
||||
|
||||
// LoadPhotosForMonth loads photos for a specific month.
|
||||
func (s *PhotoStore) LoadPhotosForMonth(year int, month time.Month) ([]*models.Photo, error) {
|
||||
allPhotos, err := s.LoadAllPhotos()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var monthPhotos []*models.Photo
|
||||
for _, photo := range allPhotos {
|
||||
if photo.Date.Year() == year && photo.Date.Month() == month {
|
||||
monthPhotos = append(monthPhotos, photo)
|
||||
}
|
||||
}
|
||||
|
||||
return monthPhotos, nil
|
||||
}
|
||||
|
||||
// LoadPhotosForDate loads photos for a specific date.
|
||||
func (s *PhotoStore) LoadPhotosForDate(date time.Time) ([]*models.Photo, error) {
|
||||
allPhotos, err := s.LoadAllPhotos()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetDate := date.Truncate(24 * time.Hour)
|
||||
var datePhotos []*models.Photo
|
||||
|
||||
for _, photo := range allPhotos {
|
||||
if photo.Date.Truncate(24 * time.Hour).Equal(targetDate) {
|
||||
datePhotos = append(datePhotos, photo)
|
||||
}
|
||||
}
|
||||
|
||||
return datePhotos, nil
|
||||
}
|
||||
|
||||
// GetPhotoPath returns the full filesystem path for a photo.
|
||||
func (s *PhotoStore) GetPhotoPath(year, filename string) string {
|
||||
return filepath.Join(s.rootFolder, year, filename)
|
||||
}
|
||||
|
||||
func (s *PhotoStore) loadPhotosFromYear(yearFolder, yearPath string) ([]*models.Photo, error) {
|
||||
var photos []*models.Photo
|
||||
|
||||
entries, err := os.ReadDir(yearPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := entry.Name()
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
if !supportedExtensions[ext] {
|
||||
continue
|
||||
}
|
||||
|
||||
photo := parsePhotoFilename(yearFolder, filename, filepath.Join(yearPath, filename))
|
||||
if photo != nil {
|
||||
photos = append(photos, photo)
|
||||
}
|
||||
}
|
||||
|
||||
return photos, nil
|
||||
}
|
||||
|
||||
func parsePhotoFilename(yearFolder, filename, fullPath string) *models.Photo {
|
||||
matches := filenamePattern.FindStringSubmatch(filename)
|
||||
if matches == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dateStr := fmt.Sprintf("%s-%s-%s", matches[1], matches[2], matches[3])
|
||||
date, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
description := matches[4]
|
||||
// Convert underscores and camelCase to spaces for display
|
||||
description = strings.ReplaceAll(description, "_", " ")
|
||||
|
||||
return &models.Photo{
|
||||
Date: date,
|
||||
Year: yearFolder,
|
||||
Filename: filename,
|
||||
Description: description,
|
||||
FullPath: fullPath,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user