package storage import ( "fmt" "os" "path/filepath" "regexp" "strings" "time" "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, } }