Initial implementation
This commit is contained in:
185
internal/storage/caldav.go
Normal file
185
internal/storage/caldav.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-ical"
|
||||
"github.com/emersion/go-webdav/caldav"
|
||||
"github.com/luxick/chronological/internal/models"
|
||||
)
|
||||
|
||||
// CalDAVStore handles fetching events from a CalDAV server.
|
||||
type CalDAVStore struct {
|
||||
url string
|
||||
username string
|
||||
password string
|
||||
client *caldav.Client
|
||||
}
|
||||
|
||||
// NewCalDAVStore creates a new CalDAV store with the given credentials.
|
||||
func NewCalDAVStore(url, username, password string) *CalDAVStore {
|
||||
return &CalDAVStore{
|
||||
url: url,
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes a connection to the CalDAV server.
|
||||
func (s *CalDAVStore) Connect(ctx context.Context) error {
|
||||
httpClient := &http.Client{
|
||||
Transport: &basicAuthTransport{
|
||||
username: s.username,
|
||||
password: s.password,
|
||||
},
|
||||
}
|
||||
|
||||
client, err := caldav.NewClient(httpClient, s.url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create caldav client: %w", err)
|
||||
}
|
||||
|
||||
s.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadEvents fetches events from the CalDAV server for a date range.
|
||||
func (s *CalDAVStore) LoadEvents(ctx context.Context, start, end time.Time) ([]*models.Event, error) {
|
||||
if s.client == nil {
|
||||
return nil, fmt.Errorf("caldav client not connected")
|
||||
}
|
||||
|
||||
principal, err := s.client.FindCurrentUserPrincipal(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find user principal: %w", err)
|
||||
}
|
||||
|
||||
homeSet, err := s.client.FindCalendarHomeSet(ctx, principal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find calendar home set: %w", err)
|
||||
}
|
||||
|
||||
calendars, err := s.client.FindCalendars(ctx, homeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find calendars: %w", err)
|
||||
}
|
||||
|
||||
var allEvents []*models.Event
|
||||
|
||||
for _, cal := range calendars {
|
||||
query := &caldav.CalendarQuery{
|
||||
CompRequest: caldav.CalendarCompRequest{
|
||||
Name: "VCALENDAR",
|
||||
Comps: []caldav.CalendarCompRequest{{
|
||||
Name: "VEVENT",
|
||||
Props: []string{
|
||||
"SUMMARY",
|
||||
"DESCRIPTION",
|
||||
"DTSTART",
|
||||
"DTEND",
|
||||
"LOCATION",
|
||||
"UID",
|
||||
},
|
||||
}},
|
||||
},
|
||||
CompFilter: caldav.CompFilter{
|
||||
Name: "VCALENDAR",
|
||||
Comps: []caldav.CompFilter{{
|
||||
Name: "VEVENT",
|
||||
Start: start,
|
||||
End: end,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
objects, err := s.client.QueryCalendar(ctx, cal.Path, query)
|
||||
if err != nil {
|
||||
continue // Skip calendars that fail
|
||||
}
|
||||
|
||||
events, err := parseCalDAVObjects(objects)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
allEvents = append(allEvents, events...)
|
||||
}
|
||||
|
||||
return allEvents, nil
|
||||
}
|
||||
|
||||
// LoadEventsForMonth fetches events for a specific month.
|
||||
func (s *CalDAVStore) LoadEventsForMonth(ctx context.Context, year int, month time.Month) ([]*models.Event, error) {
|
||||
start := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
|
||||
end := start.AddDate(0, 1, 0)
|
||||
return s.LoadEvents(ctx, start, end)
|
||||
}
|
||||
|
||||
func parseCalDAVObjects(objects []caldav.CalendarObject) ([]*models.Event, error) {
|
||||
var events []*models.Event
|
||||
|
||||
for _, obj := range objects {
|
||||
cal := obj.Data
|
||||
if cal == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, child := range cal.Children {
|
||||
if child.Name != ical.CompEvent {
|
||||
continue
|
||||
}
|
||||
|
||||
event := ical.Event{Component: child}
|
||||
|
||||
uid, _ := event.Props.Text(ical.PropUID)
|
||||
summary, _ := event.Props.Text(ical.PropSummary)
|
||||
description, _ := event.Props.Text(ical.PropDescription)
|
||||
location, _ := event.Props.Text(ical.PropLocation)
|
||||
|
||||
dtstart, err := event.Props.DateTime(ical.PropDateTimeStart, time.Local)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dtend, err := event.Props.DateTime(ical.PropDateTimeEnd, time.Local)
|
||||
if err != nil {
|
||||
dtend = dtstart.Add(time.Hour)
|
||||
}
|
||||
|
||||
// Check if all-day event
|
||||
prop := event.Props.Get(ical.PropDateTimeStart)
|
||||
allDay := false
|
||||
if prop != nil {
|
||||
valueParam := prop.Params.Get(ical.ParamValue)
|
||||
allDay = valueParam == "DATE"
|
||||
}
|
||||
|
||||
events = append(events, &models.Event{
|
||||
ID: uid,
|
||||
Title: summary,
|
||||
Description: description,
|
||||
Start: dtstart,
|
||||
End: dtend,
|
||||
AllDay: allDay,
|
||||
Location: location,
|
||||
Source: models.SourceCalDAV,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// basicAuthTransport adds basic auth to HTTP requests.
|
||||
type basicAuthTransport struct {
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func (t *basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.SetBasicAuth(t.username, t.password)
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
341
internal/storage/ics.go
Normal file
341
internal/storage/ics.go
Normal file
@@ -0,0 +1,341 @@
|
||||
// Package storage handles reading and writing data from various sources.
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-ical"
|
||||
"github.com/luxick/chronological/internal/models"
|
||||
)
|
||||
|
||||
// ICSStore handles reading and writing ICS calendar files.
|
||||
type ICSStore struct {
|
||||
calendarPath string
|
||||
diaryPath string
|
||||
}
|
||||
|
||||
// NewICSStore creates a new ICS store with the given file paths.
|
||||
func NewICSStore(calendarPath, diaryPath string) *ICSStore {
|
||||
return &ICSStore{
|
||||
calendarPath: calendarPath,
|
||||
diaryPath: diaryPath,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadEvents loads all events from the calendar ICS file.
|
||||
func (s *ICSStore) LoadEvents() ([]*models.Event, error) {
|
||||
if s.calendarPath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(s.calendarPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to open calendar file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
dec := ical.NewDecoder(file)
|
||||
cal, err := dec.Decode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode calendar: %w", err)
|
||||
}
|
||||
|
||||
return parseEventsFromCalendar(cal, models.SourceLocal)
|
||||
}
|
||||
|
||||
// LoadEventsForMonth loads events that occur within the specified month.
|
||||
func (s *ICSStore) LoadEventsForMonth(year int, month time.Month) ([]*models.Event, error) {
|
||||
allEvents, err := s.LoadEvents()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
|
||||
end := start.AddDate(0, 1, 0)
|
||||
|
||||
var monthEvents []*models.Event
|
||||
for _, event := range allEvents {
|
||||
if eventInRange(event, start, end) {
|
||||
monthEvents = append(monthEvents, event)
|
||||
}
|
||||
}
|
||||
|
||||
return monthEvents, nil
|
||||
}
|
||||
|
||||
// LoadDiaryEntries loads all diary entries from the diary ICS file.
|
||||
func (s *ICSStore) LoadDiaryEntries() ([]*models.DiaryEntry, error) {
|
||||
if s.diaryPath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(s.diaryPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to open diary file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
dec := ical.NewDecoder(file)
|
||||
cal, err := dec.Decode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode diary calendar: %w", err)
|
||||
}
|
||||
|
||||
return parseDiaryFromCalendar(cal)
|
||||
}
|
||||
|
||||
// LoadDiaryEntry loads a single diary entry for a specific date.
|
||||
func (s *ICSStore) LoadDiaryEntry(date time.Time) (*models.DiaryEntry, error) {
|
||||
entries, err := s.LoadDiaryEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetDate := date.Truncate(24 * time.Hour)
|
||||
for _, entry := range entries {
|
||||
if entry.Date.Truncate(24 * time.Hour).Equal(targetDate) {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveDiaryEntry saves or updates a diary entry for a specific date.
|
||||
func (s *ICSStore) SaveDiaryEntry(entry *models.DiaryEntry) error {
|
||||
if s.diaryPath == "" {
|
||||
return fmt.Errorf("diary path not configured")
|
||||
}
|
||||
|
||||
var cal *ical.Calendar
|
||||
|
||||
// Try to load existing calendar
|
||||
file, err := os.Open(s.diaryPath)
|
||||
if err == nil {
|
||||
dec := ical.NewDecoder(file)
|
||||
cal, err = dec.Decode()
|
||||
file.Close()
|
||||
if err != nil {
|
||||
cal = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Create new calendar if none exists
|
||||
if cal == nil {
|
||||
cal = ical.NewCalendar()
|
||||
cal.Props.SetText(ical.PropVersion, "2.0")
|
||||
cal.Props.SetText(ical.PropProductID, "-//Chronological//Diary//EN")
|
||||
}
|
||||
|
||||
targetDate := entry.Date.Truncate(24 * time.Hour)
|
||||
found := false
|
||||
|
||||
// Update existing entry if found
|
||||
for _, child := range cal.Children {
|
||||
if child.Name != ical.CompEvent {
|
||||
continue
|
||||
}
|
||||
event := ical.Event{Component: child}
|
||||
summary, _ := event.Props.Text(ical.PropSummary)
|
||||
if summary != "Chronolog" {
|
||||
continue
|
||||
}
|
||||
|
||||
dtstart, err := event.Props.DateTime(ical.PropDateTimeStart, time.Local)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if dtstart.Truncate(24 * time.Hour).Equal(targetDate) {
|
||||
event.Props.SetText(ical.PropDescription, entry.Text)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create new entry if not found
|
||||
if !found {
|
||||
event := ical.NewEvent()
|
||||
event.Props.SetText(ical.PropUID, fmt.Sprintf("chronolog-%s@chronological", entry.DateString()))
|
||||
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now())
|
||||
event.Props.SetDate(ical.PropDateTimeStart, targetDate)
|
||||
event.Props.SetDate(ical.PropDateTimeEnd, targetDate.AddDate(0, 0, 1))
|
||||
event.Props.SetText(ical.PropSummary, "Chronolog")
|
||||
event.Props.SetText(ical.PropDescription, entry.Text)
|
||||
cal.Children = append(cal.Children, event.Component)
|
||||
}
|
||||
|
||||
// Write calendar back to file
|
||||
outFile, err := os.Create(s.diaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create diary file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
enc := ical.NewEncoder(outFile)
|
||||
if err := enc.Encode(cal); err != nil {
|
||||
return fmt.Errorf("failed to encode diary calendar: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDiaryEntry removes a diary entry for a specific date.
|
||||
func (s *ICSStore) DeleteDiaryEntry(date time.Time) error {
|
||||
if s.diaryPath == "" {
|
||||
return fmt.Errorf("diary path not configured")
|
||||
}
|
||||
|
||||
file, err := os.Open(s.diaryPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to open diary file: %w", err)
|
||||
}
|
||||
|
||||
dec := ical.NewDecoder(file)
|
||||
cal, err := dec.Decode()
|
||||
file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode diary calendar: %w", err)
|
||||
}
|
||||
|
||||
targetDate := date.Truncate(24 * time.Hour)
|
||||
var newChildren []*ical.Component
|
||||
|
||||
for _, child := range cal.Children {
|
||||
keep := true
|
||||
|
||||
if child.Name == ical.CompEvent {
|
||||
event := ical.Event{Component: child}
|
||||
summary, _ := event.Props.Text(ical.PropSummary)
|
||||
if summary == "Chronolog" {
|
||||
dtstart, err := event.Props.DateTime(ical.PropDateTimeStart, time.Local)
|
||||
if err == nil && dtstart.Truncate(24*time.Hour).Equal(targetDate) {
|
||||
keep = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if keep {
|
||||
newChildren = append(newChildren, child)
|
||||
}
|
||||
}
|
||||
|
||||
cal.Children = newChildren
|
||||
|
||||
outFile, err := os.Create(s.diaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create diary file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
enc := ical.NewEncoder(outFile)
|
||||
if err := enc.Encode(cal); err != nil {
|
||||
return fmt.Errorf("failed to encode diary calendar: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseEventsFromCalendar(cal *ical.Calendar, source models.EventSource) ([]*models.Event, error) {
|
||||
var events []*models.Event
|
||||
|
||||
for _, child := range cal.Children {
|
||||
if child.Name != ical.CompEvent {
|
||||
continue
|
||||
}
|
||||
|
||||
event := ical.Event{Component: child}
|
||||
|
||||
uid, _ := event.Props.Text(ical.PropUID)
|
||||
summary, _ := event.Props.Text(ical.PropSummary)
|
||||
description, _ := event.Props.Text(ical.PropDescription)
|
||||
location, _ := event.Props.Text(ical.PropLocation)
|
||||
|
||||
dtstart, err := event.Props.DateTime(ical.PropDateTimeStart, time.Local)
|
||||
if err != nil {
|
||||
// Try date-only format for all-day events
|
||||
dtstart, err = event.Props.DateTime(ical.PropDateTimeStart, time.Local)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
dtend, err := event.Props.DateTime(ical.PropDateTimeEnd, time.Local)
|
||||
if err != nil {
|
||||
dtend = dtstart.Add(time.Hour)
|
||||
}
|
||||
|
||||
// Check if all-day event (date-only, no time component)
|
||||
allDay := isAllDayEvent(&event)
|
||||
|
||||
events = append(events, &models.Event{
|
||||
ID: uid,
|
||||
Title: summary,
|
||||
Description: description,
|
||||
Start: dtstart,
|
||||
End: dtend,
|
||||
AllDay: allDay,
|
||||
Location: location,
|
||||
Source: source,
|
||||
})
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func parseDiaryFromCalendar(cal *ical.Calendar) ([]*models.DiaryEntry, error) {
|
||||
var entries []*models.DiaryEntry
|
||||
|
||||
for _, child := range cal.Children {
|
||||
if child.Name != ical.CompEvent {
|
||||
continue
|
||||
}
|
||||
|
||||
event := ical.Event{Component: child}
|
||||
|
||||
summary, _ := event.Props.Text(ical.PropSummary)
|
||||
if summary != "Chronolog" {
|
||||
continue
|
||||
}
|
||||
|
||||
dtstart, err := event.Props.DateTime(ical.PropDateTimeStart, time.Local)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
description, _ := event.Props.Text(ical.PropDescription)
|
||||
|
||||
entries = append(entries, &models.DiaryEntry{
|
||||
Date: dtstart.Truncate(24 * time.Hour),
|
||||
Text: description,
|
||||
})
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func isAllDayEvent(event *ical.Event) bool {
|
||||
prop := event.Props.Get(ical.PropDateTimeStart)
|
||||
if prop == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// All-day events have VALUE=DATE parameter
|
||||
valueParam := prop.Params.Get(ical.ParamValue)
|
||||
return valueParam == "DATE"
|
||||
}
|
||||
|
||||
func eventInRange(event *models.Event, start, end time.Time) bool {
|
||||
return event.Start.Before(end) && event.End.After(start)
|
||||
}
|
||||
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