Camera Workflow automatically organizes your media files by date using metadata extraction, creating a clean, chronological archive.
Directory Structure
Default Organization
With --organize-by-date (enabled by default), files are organized hierarchically:
destination/
├── 2024/
│ ├── 03-March/
│ │ ├── 2024-03-15/
│ │ │ ├── images/
│ │ │ │ ├── 20240315_IMG_1234.avif
│ │ │ │ ├── 20240315_IMG_1235.avif
│ │ │ │ └── 20240315_DSC_5678.avif
│ │ │ └── videos/
│ │ │ ├── 20240315_VID_1001.mp4
│ │ │ └── 20240315_VID_1002.mp4
│ │ └── 2024-03-16/
│ │ ├── images/
│ │ └── videos/
│ └── 04-April/
│ └── 2024-04-01/
│ ├── images/
│ └── videos/
└── 2025/
└── 01-January/
└── 2025-01-01/
├── images/
└── videos/
Structure breakdown:
YYYY/ - Year folder
MM-Month/ - Month folder with localized name
YYYY-MM-DD/ - Day folder
images/ or videos/ - Media type separation
YYYYMMDD_filename.ext - Date-prefixed files
Flat Organization
Disable date organization to keep files at the root:
media-converter --organize-by-date=false /source /dest
destination/
├── 20240315_IMG_1234.avif
├── 20240315_IMG_1235.avif
├── 20240315_VID_1001.mp4
├── 20240316_IMG_5678.avif
└── 20240316_VID_1002.mp4
Even with flat organization, files are still prefixed with their creation date (YYYYMMDD_).
Dates are extracted using multiple methods in priority order:
macOS Metadata (mdls)
Most reliable for RAW filesmdls -name kMDItemContentCreationDate IMG_1234.CR2
# kMDItemContentCreationDate = 2024-03-15 14:23:45 +0000
Works for:
- RAW formats (CR2, NEF, ARW, DNG)
- HEIC/HEIF
- MOV/MP4 videos
- JPG with EXIF
EXIF Data (ImageMagick)
Standard for photosmagick identify -format "%[EXIF:DateTimeOriginal]" IMG_1234.JPG
# 2024:03:15 14:23:45
Reads EXIF:DateTimeOriginal tag from:
- JPG/JPEG
- TIFF
- Some RAW formats
Video Metadata (FFprobe)
For video filesffprobe -v quiet -show_entries format_tags=creation_time -of default=noprint_wrappers=1:nokey=1 VID_1234.MOV
# 2024-03-15T14:23:45.000000Z
Reads creation_time from: Filesystem Modification Time
Fallback methodinfo, _ := os.Stat(filePath)
modTime := info.ModTime()
Only used if metadata extraction fails. May not reflect actual creation date.
func GetFileDate(filePath string) (time.Time, error) {
// Try macOS metadata first (most reliable)
if runtime.GOOS == "darwin" {
if date, err := getDateFromMDLS(filePath); err == nil {
return date, nil
}
}
// Try EXIF for images
if HasExtension(filePath, photoFormats) {
if date, err := getDateFromEXIF(filePath); err == nil {
return date, nil
}
}
// Try video metadata
if HasExtension(filePath, videoFormats) {
if date, err := getDateFromVideo(filePath); err == nil {
return date, nil
}
}
// Fallback to file modification time
info, err := os.Stat(filePath)
if err != nil {
return time.Time{}, err
}
return info.ModTime(), nil
}
Dates are extracted from the original file before conversion to ensure accurate organization.
Filename Generation
Date Prefix
All files are prefixed with their creation date:
baseName := utils.CleanFilename(name, format, fileDate, 1)
// IMG_1234.CR2 + 2024-03-15 → 20240315_IMG_1234.avif
Duplicate Handling
If a file with the same name exists, a counter is appended:
20240315_IMG_1234.avif # First file
20240315_IMG_1234_1.avif # Duplicate name
20240315_IMG_1234_2.avif # Another duplicate
Code:
func GetUniqueFilename(dir, baseName, ext string) (string, error) {
counter := 1
name := baseName
for {
path := filepath.Join(dir, name+ext)
if _, err := os.Stat(path); os.IsNotExist(err) {
return path, nil
}
name = fmt.Sprintf("%s_%d", baseName, counter)
counter++
}
}
Localization
Supported Languages
Month names support multiple languages:
English (default)
French
Spanish
German
media-converter --language en /source /dest
2024/
├── 01-January/
├── 02-February/
├── 03-March/
└── ...
media-converter --language fr /source /dest
2024/
├── 01-Janvier/
├── 02-Février/
├── 03-Mars/
└── ...
media-converter --language es /source /dest
2024/
├── 01-Enero/
├── 02-Febrero/
├── 03-Marzo/
└── ...
media-converter --language de /source /dest
2024/
├── 01-Januar/
├── 02-Februar/
├── 03-März/
└── ...
Language Implementation
var monthNames = map[string][]string{
"en": {"January", "February", "March", ...},
"fr": {"Janvier", "Février", "Mars", ...},
"es": {"Enero", "Febrero", "Marzo", ...},
"de": {"Januar", "Februar", "März", ...},
}
func GetMonthName(month int, language string) string {
names, ok := monthNames[language]
if !ok {
names = monthNames["en"] // Fallback to English
}
return names[month-1]
}
Path Generation
CreateDestinationPath Function
func CreateDestinationPath(baseDir string, fileDate time.Time, mediaType string, organizeByDate bool, language string) string {
if !organizeByDate {
return baseDir // Flat organization
}
year := fileDate.Format("2006")
month := fileDate.Format("01")
day := fileDate.Format("2006-01-02")
monthName := GetMonthName(int(fileDate.Month()), language)
// Build path: baseDir/YYYY/MM-MonthName/YYYY-MM-DD/mediaType/
return filepath.Join(
baseDir,
year,
fmt.Sprintf("%s-%s", month, monthName),
day,
mediaType, // "images" or "videos"
)
}
Usage:
// For an image taken on 2024-03-15
destPath := CreateDestinationPath(
"/Volumes/Archive",
time.Date(2024, 3, 15, 14, 23, 45, 0, time.UTC),
"image",
true, // organize by date
"en",
)
// Result: /Volumes/Archive/2024/03-March/2024-03-15/images/
Type Detection
Files are separated into images/ or videos/ folders:
func DetectMediaType(path string, photoFormats, videoFormats []string) string {
if HasExtension(path, photoFormats) {
return "image"
}
if HasExtension(path, videoFormats) {
return "video"
}
return "unknown"
}
Photo formats:
- CR2, NEF, ARW, DNG (RAW)
- JPG, JPEG
- PNG
- HEIC, HEIF
- TIFF, TIF
- WebP, AVIF (already converted)
Video formats:
- MOV, MP4
- AVI, MKV
- M4V, 3GP
- MTS, M2TS
Format detection is case-insensitive (.JPG and .jpg are treated the same).
Use Cases
Chronological Photo Archive
media-converter \
--organize-by-date \
--language en \
--photo-format avif \
/Photos/Unorganized /Photos/Archive
Creates a clean, browsable archive:
Archive/
├── 2023/
│ └── 12-December/
│ └── 2023-12-25/
│ └── images/
│ ├── 20231225_Christmas_001.avif
│ ├── 20231225_Christmas_002.avif
│ └── 20231225_Family_001.avif
└── 2024/
├── 01-January/
└── 02-February/
media-converter \
--organize-by-date \
--photo-format avif \
--video-codec h265 \
/DCIM /Archive
Photos and videos from the same day in separate folders:
2024/03-March/2024-03-15/
├── images/
│ ├── 20240315_IMG_1234.avif
│ └── 20240315_IMG_1235.avif
└── videos/
├── 20240315_VID_1001.mp4
└── 20240315_VID_1002.mp4
Flat Archive (No Organization)
media-converter \
--organize-by-date=false \
--photo-format webp \
/source /dest
All files in destination root with date prefixes:
dest/
├── 20240315_IMG_1234.webp
├── 20240315_IMG_1235.webp
├── 20240316_IMG_5678.webp
└── 20240317_VID_1001.mp4
Edge Cases
If date extraction fails entirely:
⚠️ Could not extract date from IMG_9999.JPG: using file modification time
File is organized using filesystem modification time as fallback.
Invalid Dates
Dates are validated before use:
if fileDate.IsZero() || fileDate.Year() < 1990 || fileDate.Year() > 2100 {
// Use filesystem modification time instead
fileDate = info.ModTime()
}
Timezone Handling
All dates are normalized to local time:
fileDate = fileDate.Local()
This ensures consistent organization regardless of timezone in EXIF data.
Configuration File
Store organization preferences:
# Organization settings
organize_by_date: true
language: en
# File naming
# (date prefix is always added, no configuration needed)
EXIF Preservation
During conversion, EXIF metadata (including dates) is preserved in output files.
cmd = exec.CommandContext(ctx, "magick", inputPath,
"-define", "avif:preserve-exif=true",
"-define", "webp:preserve-exif=true",
...)
This ensures:
- Original dates remain in converted files
- Re-running organization produces same structure
- Metadata viewers show correct creation dates
Copy-Only Mode
Date organization works identically in copy-only mode:
media-converter --copy-only --organize-by-date /source /dest
dest/
├── 2024/
│ └── 03-March/
│ └── 2024-03-15/
│ ├── images/
│ │ ├── IMG_1234.CR2 # Original RAW preserved
│ │ └── IMG_1235.JPG
│ └── videos/
│ └── VID_1001.MOV
Files keep their original extensions but are organized by date.
Date extraction adds minimal overhead:
- mdls (macOS): ~10-20ms per file
- EXIF reading: ~5-10ms per file
- FFprobe: ~20-50ms per file
Extraction happens once per file during initial scan, not during conversion.
Directory Creation
Directories are created lazily:
func EnsureDir(path string) error {
return os.MkdirAll(path, 0755)
}
Only creates directories as needed, avoiding unnecessary filesystem operations.