Automatically organize photos and videos on a Synology NAS by reading EXIF
metadata and placing files into a dated folder hierarchy (ANNEE/MOIS).
Designed for the DS216play (ARMv7l, 512 MB RAM) running DSM 7.x with minimal CPU and memory footprint. Runs as a native Synology SPK service.
- Polls one or more input folders every N seconds (default: 30s, configurable)
- Photos: reads EXIF
DateTimeOriginal(tag 0x9003) — JPEG, HEIC, PNG, TIFF - Videos: reads QuickTime
mvhdcreation date (UTC → local time) — MP4, MOV, AVI, MKV… - Files without the required metadata tag are skipped — no fallback, no data loss
- Moves files to
output/YYYY/MM/according to a configurable naming pattern - Handles filename conflicts:
rename,skip, oroverwrite - Structured JSON logging compatible with DSM log center
- Graceful shutdown on SIGTERM (DSM stop command)
- Statically linked (musl) — no GLIBC dependency, works on any DSM kernel
every poll_interval_secs (default: 30s)
│
watcher (scan input folder)
│
processor
├── validate extension
├── read capture date
│ ├── photo → EXIF DateTimeOriginal (tag 0x9003)
│ └── video → QuickTime mvhd creation_time (UTC → local)
├── apply naming pattern
├── resolve conflicts
├── create directories
└── move (rename syscall, fallback copy+delete)
| Requirement | Value |
|---|---|
| NAS architecture | DS216play (STM Monaco STiH412 — ARMv7l) |
| DSM version | 7.0 minimum (tested on DSM 7.1.1) |
| RAM | 512 MB minimum |
Other monaco-platform models (DS215play, DS214play) should also work. Models with a different SoC (DS216, DS116 — armada38x platform) are not compatible with this package.
- In DSM: Package Center → Settings → Package Sources → Add
- Name:
Syno Media Organizer - URL:
https://jordanatdown.github.io/syno-media-organizer/packages.json
- Name:
- Click OK, then search for Syno Media Organizer in Package Center and install
- Future updates appear automatically in Package Center
- Download the latest
.spkfrom Releases - In DSM: Package Center → Manual Install → select the
.spkfile
After installing (either method):
- Grant shared folder permissions (required — see section below)
- Edit the config via File Station:
- Open File Station → shared folder
config→syno-media-organizer→config.toml - Right-click → Open with Text Editor (install the free Text Editor package if needed)
- Or via SSH:
vi /volume1/config/syno-media-organizer/config.toml
- Open File Station → shared folder
- Start the service from Package Center → Syno Media Organizer → Run
- Check logs (SSH):
tail -f /var/packages/syno-media-organizer/var/syno-media-organizer.log
Upgrading: existing
config.tomlis automatically migrated and preserved. Uninstall: Package Center → Syno Media Organizer → Uninstall
The service runs as a dedicated system user syno-media-organizer. Synology shared folders
restrict access by user — you must explicitly grant this user access to every shared folder
the tool reads from or writes to.
For each shared folder used as input or output in your config:
- Panneau de configuration → Dossier partagé
- Select the folder → Modifier → tab Permissions
- Click Ajouter → search for user
syno-media-organizer - Grant: Lecture/Écriture (Read/Write) on input folders; Lecture/Écriture on output folders
The
configshared folder (whereconfig.tomllives) is handled automatically by the installer — no manual action needed for it.
# Scan interval in seconds (optional, default: 30)
poll_interval_secs = 30
[[folders]]
input = "/volume1/inbox/camera"
output = "/volume1/Phototheque"
pattern = "{year}/{month}/{prefix}{year}-{month}-{day}_{hour}{min}{sec}_{stem}{ext}"
recursive = true
photo_prefix = "IMG_" # optional, default: ""
video_prefix = "VID_" # optional, default: ""
on_conflict = "rename" # rename | skip | overwrite
extensions = ["jpg", "jpeg", "png", "heic", "mp4", "mov", "avi", "mkv"]
# excluded_dirs = ["@eaDir", "@SynoEAStream", "@Recycle", "#recycle", "@tmp"]The excluded_dirs option controls which directory names are silently skipped during scanning
(matched against every component of the file path, not just the top-level folder).
Default value — automatically excludes all Synology DSM hidden folders:
| Directory | Purpose |
|---|---|
@eaDir |
Thumbnails and metadata generated by Synology Photos / Photo Station |
@SynoEAStream |
Extended attribute streams (DSM filesystem feature) |
@Recycle |
Shared folder recycle bin |
#recycle |
Alternate recycle bin name (depends on DSM version) |
@tmp |
DSM temporary files |
Other DSM directories you may want to add depending on your setup:
| Directory | Purpose |
|---|---|
@sharebin |
Shared folder trash (older DSM versions) |
@database |
Synology Photos internal database |
@photos |
Synology Photos working directory |
@Recently-Snapshot |
Btrfs snapshot staging area |
To override the default list, add excluded_dirs to any [[folders]] block:
excluded_dirs = ["@eaDir", "@SynoEAStream", "@Recycle", "#recycle", "@tmp", "my-custom-dir"]Files that have no capture date metadata (no EXIF DateTimeOriginal, no QuickTime
mvhd creation time) cannot be organised. By default the watcher logs a WARN the
first time it encounters such a file and then remembers it in a persistent cache
(no_date_cache.json, stored next to config.toml). On subsequent scan cycles the
file is silently skipped.
The cache entry is automatically invalidated when:
- The file's modification time changes (e.g. you add EXIF with
exiftool) — the file will be re-tried on the next scan. - The TTL expires — if
no_date_cache_ttl_daysis set, the file is retried after that many days regardless of mtime.
Global config options (add to config.toml, outside any [[folders]] block):
# default: true — set false to always re-scan all files, even those without metadata
no_date_cache_enabled = true
# default: 0 (never expire) — set e.g. 30 to retry files without metadata once a month
no_date_cache_ttl_days = 0To reset the cache manually, delete the file:
rm /volume1/config/syno-media-organizer/no_date_cache.jsonThe cache is also deleted automatically when the package is uninstalled from Package
Center (your config.toml is preserved).
| Token | Description |
|---|---|
{year} |
4-digit year (2024) |
{month} |
2-digit month (01–12) |
{day} |
2-digit day (01–31) |
{hour} |
2-digit hour (00–23) |
{min} |
2-digit minute |
{sec} |
2-digit second |
{stem} |
Original filename without extension |
{ext} |
Extension with dot (.jpg) |
{camera} |
Camera model from EXIF (or unknown) |
{counter} |
Auto-increment counter (0001, 0002, …) |
{prefix} |
photo_prefix for photos, video_prefix for videos (default: "") |
| Extension | Format |
|---|---|
.jpg / .jpeg |
JPEG |
.heic / .heif |
High Efficiency Image (iPhone) |
.png |
PNG with EXIF APP1 block |
.tiff / .tif |
TIFF |
The file must contain a valid EXIF block with a DateTimeOriginal field.
If the tag is absent the file is skipped — no fallback, file stays in input.
| Extension | Format |
|---|---|
.mp4 |
MPEG-4 |
.mov |
QuickTime Movie (iPhone, cameras) |
.avi |
Audio Video Interleave |
.mkv |
Matroska |
.3gp |
3GPP (Android) |
.m4v |
iTunes Video |
.wmv |
Windows Media Video |
.flv |
Flash Video |
.webm |
WebM |
.ts / .mts / .m2ts |
MPEG-2 Transport Stream |
The creation_time field is read from the mvhd (Movie Header Box) inside the moov
container. It is stored as UTC (seconds since 1904-01-01, Mac epoch) and converted
to local time at runtime. If the moov box is absent the file is skipped.
Only extensions listed in the
extensionsconfig key are processed. You must explicitly include the extensions you want (e.g.["jpg", "heic", "mp4", "mov"]).
- WSL Ubuntu 24.04
- Rust stable (
rustup) git config core.hooksPath .githooks(activate hooks)
bash scripts/setup-cross.shcargo fmt
cargo clippy -- -D warnings
cargo test --lib # unit tests
cargo test # all tests (unit + integration)
cargo zigbuild --release --target armv7-unknown-linux-musleabihf
bash scripts/build-spk.sh # build the .spk package# 1. Update CHANGELOG.md
# 2. Run:
bash scripts/release.sh X.Y.Z
git push origin master --tagsGitHub Actions will cross-compile, package the .spk, and publish the release.
- Follow Conventional Commits
- All commits must pass
cargo fmt --check,cargo clippy -D warnings, and unit tests (enforced by pre-commit hook) - No binary test fixtures — generate them at runtime with
tests/common/