Allows batch moving/copying photos to a new location organized by time.
The following command will copy all files in /input/path to /output/path/with/variables where each file will retain its relative directory but will be placed in addition year/month/extension directory inside the relative structure. Duplicate files will be appended with -number (eg. -1). And all files will be moved instead of copied.
./PhotoCopy -i "/input/path" -o "/output/path/with/variables/{directory}/{year}/{month}/{extension}/{name}" --duplicate-format "-{number}" --mode "Move"
-i, --input Required. Path to a source directory, which will be scanned for files.
-o, --output Required. Destination path for the operation. Determines the final path files have. Supported variables (case-sensitive): {year}, {month}, {day}, {dayOfYear}, {directory}, {name}, {nameNoExtension}, {extension}, {camera}, {album}
- {year} - year extracted from file creation date or exif
- {month} - month extracted from file creation date or exif
- {day} - day extracted from file creation date or exif
- {dayOfYear} - day of year (~1-365) from file creation date or exif
- {directory} - relative directory structure from input path
- {name} - file name with extension
- {nameNoExtension} - file name without extension
- {extension} - file extension
- {camera} - camera make/model from EXIF metadata (e.g., "Canon EOS R5")
- {album} - album name from EXIF/XMP/IPTC metadata
-d, --dry Only prints what will happen without actually doing it. It is recommended to combine it with log level verbose.
-m, --mode (Default: copy) Operation mode. Available modes: copy, move
-l, --logLevel (Default: important) Determines how much information is printed on the screen. Options: verbose, important, errorsOnly
--no-skip-duplicate Disables duplicate skipping.
__--duplicate-format (Default: _{number}) Format used for differentiating files with the same name. Use {number} for number placeholder.
--skip-existing Skips file if it already exists in the output.
--max-depth Maximum directory recursion depth when scanning for files. 0 or omit for unlimited (default), 1 = root directory only, 2 = root + one level of subdirectories, etc.
PhotoCopy supports organizing photos by geographic location using GPS metadata from your photos. Location variables include:
{district}- Neighborhood or district name{city}- City name{county}- County/region name{state}- State/province name{country}- Country name or code
When a photo lacks GPS metadata or the location can't be determined, PhotoCopy provides flexible options for handling these cases.
Set a default fallback in appsettings.yaml:
photoCopy:
unknownLocationFallback: "Unknown" # Default folder for photos without locationOverride the global fallback for specific variables using the pipe (|) syntax:
# Use "NoLocation" instead of the global fallback
./PhotoCopy -i "/photos" -o "/sorted/{year}/{city|NoLocation}/{name}"Try multiple location levels in order, falling back through the chain:
# Try city first, then country if city is unavailable
./PhotoCopy -i "/photos" -o "/sorted/{year}/{city|country}/{name}"
# Three-level chain: try district, then city, then country
./PhotoCopy -i "/photos" -o "/sorted/{year}/{district|city|country}/{name}"
# Chain ending with a literal fallback
./PhotoCopy -i "/photos" -o "/sorted/{year}/{city|country|Unknown}/{name}"Omit a folder entirely when the variable is unavailable:
# If city is unknown, the folder level is simply omitted
./PhotoCopy -i "/photos" -o "/sorted/{year}/{city|}/{month}/{name}"
# Photos with city: /sorted/2024/Prague/01/photo.jpg
# Photos without: /sorted/2024/01/photo.jpgUse location values only when you have enough photos to justify a separate folder:
# Only create city folders if there are at least 10 photos from that city
./PhotoCopy -i "/photos" -o "/sorted/{year}/{city?min=10|country}/{name}"How it works: PhotoCopy performs a two-pass operation:
- First pass: Scans all files and counts photos per location
- Second pass: Applies thresholds and organizes files
Example results with {city?min=10|country}:
Source: 15 photos from Prague, 3 from Brno, 8 from Vienna
Output:
- Prague photos → /sorted/2024/Prague/photo.jpg (15 ≥ 10, uses city)
- Brno photos → /sorted/2024/CZ/photo.jpg (3 < 10, falls back to country)
- Vienna photos → /sorted/2024/AT/photo.jpg (8 < 10, falls back to country)
Advanced threshold examples:
# Maximum threshold: use city only if ≤100 photos (avoid huge folders)
./PhotoCopy -i "/photos" -o "/sorted/{city?max=100|country}/{name}"
# Range: use city if between 10-100 photos
./PhotoCopy -i "/photos" -o "/sorted/{city?min=10,max=100|country}/{name}"
# Chained conditions: city if ≥10, else country if ≥5, else literal
./PhotoCopy -i "/photos" -o "/sorted/{city?min=10|country?min=5|Misc}/{name}"PhotoCopy automatically handles iPhone Live Photos, which consist of:
- A
.heicor.jpgimage with full EXIF metadata (including GPS) - A companion
.movvideo that often lacks GPS metadata
When enableLivePhotoInheritance is enabled (default: true), companion .mov videos automatically inherit GPS/location data from their paired photo file with the same base name.
Example:
Source files:
IMG_1234.heic (has GPS: Tokyo, Japan)
IMG_1234.mov (no GPS)
Result:
IMG_1234.mov inherits GPS from IMG_1234.heic
Both files will be organized to the same location folder
photoCopy:
# Enable/disable Live Photo metadata inheritance (default: true)
enableLivePhotoInheritance: true
# Related files mode - ensures paired files stay together
relatedFileMode: Strict # Strict, Loose, or None- Scanning Phase: PhotoCopy scans all files in the source directory
- Live Photo Pass: Before organization, it identifies
.heic/.jpg+.movpairs by matching base names - Metadata Transfer: Videos without GPS inherit location data from their paired photo
- Organization: Both files are organized to the same destination folder
| Photo Extension | Video Extension | Notes |
|---|---|---|
.heic |
.mov |
Standard iPhone Live Photo format |
.heif |
.mov |
Alternative HEIF format |
.jpg / .jpeg |
.mov |
Some devices use JPEG for Live Photos |
For Live Photos to stay together, ensure relatedFileMode is set to Strict or Loose:
./PhotoCopy -i "/photos" -o "/sorted/{year}/{city}/{name}" --related-file-mode StrictThis ensures both the .heic and .mov files are copied together to the same destination.