An API that downloads random photos from a Synology NAS, converts them to WebP, and serves them for slideshow clients.
GET /photos/downloadauthenticates with the Synology NAS, searches configured folders, downloads a random set, unzips, flattens the folder structure, and converts images to WebP.- Photos are served as static files under
/slideshow. GET /photos/slidesreturns metadata for the files currently in the slideshow folder.- An optional scheduled background job powered by Hangfire can be configured to automatically download and process new photo sets at set intervals.
The API is intended to run on your Synology NAS and be accessed only from your local network.
- Architecture
- Endpoints
- Real-time Updates
- Features
- Logging
- Local Development
- Docker (Local)
- Deployment To Your Synology NAS Device
- Important !!!!!!!
- Future Enhancements
- Client App
- Shameless Plug
- Giving Back
For a detailed breakdown of call flows and component interactions, see Architecture Overview.
graph TD
Client[Slideshow Client]
API[ASP.NET Core API]
NAS[Synology NAS]
FS[Local File System]
SignalR[SignalR Hub]
subgraph Background Processing
PhotoChannel[Photo Processing Channel]
PhotoWorker[Photo Processing Worker]
Hangfire[Hangfire Scheduled Job]
end
Client -->|HTTP Request| API
API -->|Search & Download| NAS
API -->|Extract| FS
API -->|Publish| PhotoChannel
API -->|Return 204| Client
PhotoWorker -->|Read| PhotoChannel
PhotoWorker -->|Convert WebP| FS
PhotoWorker -->|Notify| SignalR
Hangfire -->|Execute| API
SignalR -.->|Real-time Updates| Client
Base URL (local dev): http://localhost:5097 (HTTPS in dev only: https://localhost:7078)
Base URL (running on the NAS): http://<nas-ip>:5097
The API exposes the following endpoints:
- Download Photos
- Get Photo Slides
- Get Thumbnails
- Bulk Delete Photos
GET /photos/download
- Randomly selects photos from
SynoApiOptions.FileStationSearchFolders.- Typically, the photos would be located on the
/photovolume on your NAS or your home share. e.g./[username]/Photos/PhotoLibrary.
- Typically, the photos would be located on the
- Clears the download folder first.
- Uses Synology credentials.
- Downloads, unzips, flattens, and prepares photos for processing.
- Triggers a background worker to convert photos to WebP.
This endpoint is asynchronous. It returns immediately after the photos are downloaded and the background processing is triggered. Clients can listen for real-time updates via SignalR to know when processing is complete.
Refer to "Local Development", "Docker Local", and "Deployment" for more information on configuration.
Note: There is an issue with the number of photos to download. It seems to be limited to 79; however, this number limit works for now. I will look into this later and try to figure this out.
| Status Code | Description |
|---|---|
204 |
Success |
503 |
Synology API/search failure |
500 |
Unexpected errors (Problem Details) |
An example of the error response:
{
"type": "https://datatracker.ietf.org/doc/html/rfc9110#status.503",
"title": "Failed to download photos.",
"status": 503,
"detail": "Search operation timed out after 10 attempts",
"traceId": "00-d1f393c5f6b5377af412bee5a15cd61d-89602e76756bbe5c-00"
}{
"type": "https://datatracker.ietf.org/doc/html/rfc9110#status.503",
"title": "An error occured",
"status": 500,
"detail": "[Error message]",
"traceId": "00-d1f393c5f6b5377af412bee5a15cd61d-89602e76756bbe5c-00"
}GET /photos/slides
This endpoint returns a collection of slides with info about the photos previously downloaded by the Download Photos endpoint with the following properties:
| Property | Description |
|---|---|
relativeUrl |
The relative* photo URL. e.g., /slideshow/IMG_20200323_083612.webp |
dateTaken |
The date the photo was taken Empty if missing EXIF DateTimeOriginal. If EXIF has no timezone offset, the server's local offset is used |
googleMapsLink |
A link to the photo location on Google Maps Empty if GPS metadata is missing |
location |
The photo location in the following format: City, State Empty unless geolocation is enabled and GPS metadata is valid Refer to Photo Location for more information. |
*To get the full URL of the photo, the client application needs to concatenate the base URL with this value.
| Status Code | Description |
|---|---|
200 |
Success |
500 |
Unexpected errors (Problem Details) |
An example of the success response:
[
{
"relativeUrl": "/slideshow/20250723_135938.webp",
"dateTaken": "2025-07-23 13:59:38 -07:00",
"googleMapsLink": "https://www.google.com/maps?q=37.7493922,-119.5492962",
"location": "Yosemite Valley, CA"
},
{
"relativeUrl": "/slideshow/IMG_20200323_083612.webp",
"dateTaken": "2020-03-23 08:36:12 -05:00",
"googleMapsLink": "",
"location": ""
}
]Example of the full photo's URL:
http://<your-nas-ip>:5097/slideshow/20240618_141316.jpg
GET /photos/thumbnails
This endpoint returns a list of relative URLs for thumbnail images. Thumbnails are smaller versions of the slideshow photos, optimized for gallery views.
Thumbnails are automatically generated in the background after photo processing completes. They are resized to fit within 400px (preserving the aspect ratio), have all metadata stripped, and are saved in WebP format with a __thumb postfix in the filename.
| Status Code | Description |
|---|---|
200 |
Success |
500 |
Unexpected errors (Problem Details) |
An example of the success response:
[
"/slideshow/20250723_135938__thumb.webp",
"/slideshow/IMG_20200323_083612__thumb.webp"
]POST /photos/bulk-delete
This endpoint deletes photos from the slideshow folder.
Accepts the list of photo names to delete and an optional SignalR connection ID to avoid sending a refresh signal to the requester.
The request body is a JSON object:
{
"photoNames": [
"20240303_154856.jpg",
"20240818_154700.jpg"
],
"signalRConnectionId": "optional-connection-id"
}Behavior:
- Validates the request using Fluent Validation (e.g.,
PhotoNamesis mandatory). - Deletes only from the local slideshow folder (not from the original NAS location).
- Returns a list of
unmatchedPhotos. - Returns
404if nothing matched.
| Status Code | Description |
|---|---|
200 |
Success (with unmatchedPhotos) |
400 |
Bad Request (Validation Errors) |
404 |
None matched |
500 |
Unexpected errors (Problem Details) |
Example of the validation error response:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"PhotoNames": [
"'Photo Names' must not be empty."
]
},
"traceId": "00-4cda575df9037c39e089b9f0f240b728-17f61773bce7cd46-00"
}An example of the success response:
{
"unmatchedPhotos": [
"20240818_154700.jpg",
"20250403_080106.jpg"
]
}{
"unmatchedPhotos": []
}The API uses SignalR to provide real-time updates to connected clients.
[Base URL]/hubs/slideshow
The following methods are invoked on the client:
| Method | Description |
|---|---|
RefreshSlideshow |
Triggered when photo processing is complete or when photos are deleted. |
PhotoProcessingError |
Triggered if an error occurs during background photo processing. |
RefreshGallery |
Triggered when thumbnail generation is complete. |
ThumbnailsProcessingError |
Triggered if an error occurs during background thumbnail generation. |
The API includes a scheduled background job powered by Hangfire that automatically downloads and processes new photo sets at configurable intervals. This eliminates the need to manually trigger the /photos/download endpoint.
The scheduled job is fully configurable via the PhotoDownloadScheduledJobOptions section in appsettings.json or environment variables:
| Option | Type | Description | Valid Range | Default |
|---|---|---|---|---|
Enabled |
Boolean | Enables or disables the scheduled job. | true/false | true |
DayOfWeek |
Integer | Day of the week to run the job (0 = Sunday, 1 = Monday, ..., 6 = Saturday). | 0-6 | N/A |
Hour |
Integer | Hour of the day to run the job (24-hour format). | 0-23 | N/A |
Minute |
Integer | Minute of the hour to run the job. | 0-59 | N/A |
TimeZoneId |
String | IANA time zone ID used to schedule the job (e.g., America/Chicago, America/New_York). |
Any valid IANA time zone ID | N/A |
Example configuration in appsettings.json:
{
"PhotoDownloadScheduledJobOptions": {
"Enabled": true,
"DayOfWeek": 0,
"Hour": 12,
"Minute": 3,
"TimeZoneId": "America/Chicago"
}
}This example schedules the job to run every Sunday at 12:03 PM in the America/Chicago time zone.
Notes:
- Set
Enabledtofalseto disable the scheduled job without removing the configuration. TimeZoneIdmust be a valid IANA time zone ID. See List of IANA time zones for valid values.- The Hangfire dashboard is available at
/jobsfor monitoring job execution history and status.
Logs are written to ./logs with daily rolling JSON files (for example, api-logs_20260221.json).
I might switch this to a database in the future. But for now, it's good enough.
Update the following app settings in appsettings.json or create a .NET User Secrets (Secret Manager) file:
{
"UriBase": {
"ServerIpOrHostname": "<<SERVER_IP_OR_HOSTNAME>>",
"Port": 5000
},
"SynologyUser": {
"Account": "<<ACCOUNT>>",
"Password": "<<PASSWORD>>"
},
"SynoApiOptions": {
"FileStationSearchFolders": [
"/path/to/server/photos"
],
"NumberOfPhotoDownloads": 10,
"DownloadAbsolutePath": "/path/to/slideshow/downloads"
},
"ThirdPartyServices": {
"EnableGeolocation": true,
"EnableDistributedCache": true
},
"GoogleMapsOptions": {
"ApiKey": "<<GOOGLE_MAPS_API_KEY>>",
"EnableMocks": true
},
"PhotoDownloadScheduledJobOptions": {
"Enabled": true,
"DayOfWeek": 0,
"Hour": 12,
"Minute": 3,
"TimeZoneId": "America/Chicago"
},
"ConnectionStrings": {
"Redis": "<<REDIS_CONNECTION_STRING>>"
}
}| Configuration Key | Description | Example/Default Value |
|---|---|---|
UriBase.ServerIpOrHostname |
The IP or hostname of your Synology NAS device | e.g., 192.168.1.100 or localhost |
UriBase.Port |
The port for Synology NAS devices | 5000 (default). If you are using a different port, update this value to match. |
SynologyUser.Account |
The username for your Synology NAS device | Main account or service account with file access privileges |
SynologyUser.Password |
The password for your Synology NAS device | Your account password |
SynoApiOptions.FileStationSearchFolders |
List of folders on your Synology NAS to search for photos | Must be absolute paths (e.g., /photo/family) |
SynoApiOptions.NumberOfPhotoDownloads |
The number of photos to download | Any integer value |
SynoApiOptions.DownloadAbsolutePath |
The absolute path to download photos to | Must exist before API starts, or it will throw exception at bootup |
ThirdPartyServices.EnableGeolocation |
Enable Google Maps API to get photo location | false (default). |
ThirdPartyServices.EnableDistributedCache |
Enable Redis distributed cache to speed up photo location lookup | false (default). |
GoogleMapsOptions.ApiKey |
Google Maps API key | Your API key. |
GoogleMapsOptions.EnableMocks |
Enable mock Google Maps API responses for testing | true (default). |
PhotoDownloadScheduledJobOptions.Enabled |
Enable or disable the scheduled photo download job | true (default). |
PhotoDownloadScheduledJobOptions.DayOfWeek |
Day of the week to run the job (0 = Sunday, 6 = Saturday) | 0-6 |
PhotoDownloadScheduledJobOptions.Hour |
Hour of the day to run the job (24-hour format) | 0-23 |
PhotoDownloadScheduledJobOptions.Minute |
Minute of the hour to run the job | 0-59 |
PhotoDownloadScheduledJobOptions.TimeZoneId |
IANA time zone ID used to schedule the job | e.g., America/Chicago, America/New_York |
ConnectionStrings.Redis |
Redis connection string | e.g.,localhost:6379,abortConnect=false,connectTimeout=10000 |
Refer to Endpoints on how to call the API endpoints.
Refer to Photo Location and Redis for geolocation and caching, including getting the Google Maps API.
OpenAPI is available in Development at https://localhost:7078/openapi/v1.json.
The Dockerfile creates /app/slides (SynoApiOptions.DownloadAbsolutePath) and /app/logs. The default compose file binds HTTP only:
- ASPNETCORE_URLS=http://+:5097
- ASPNETCORE_HTTP_PORTS=5097The docker-compose.yaml file is setting up an environment variable for the SynoApiOptions.DownloadAbsolutePath folder pointing to /app/slides.
I suggest creating a docker-compose.local.yml file to override some of the other app settings variables. Which is what I am doing, but not including in the repo.
Sample docker-compose.local.yml override:
services:
synology.photos.slideshow.api:
image: esausilva/synology.photos.slideshow.api:local
environment:
- UriBase:ServerIpOrHostname=<<SERVER_IP_OR_HOSTNAME>>
- UriBase:Port=<<CUSTOM_PORT>>
- SynologyUser:Account=<<ACCOUNT>>
- SynologyUser:Password=<<PASSWORD>>
- SynoApiOptions:FileStationSearchFolders:0=<<PATH_TO_PHOTOS_FOLDER_IN_NAS>>
- SynoApiOptions:FileStationSearchFolders:1=<<PATH_TO_PHOTOS_FOLDER_IN_NAS>> ## If you have more than one folder to search
- SynoApiOptions:NumberOfPhotoDownloads=10
- ThirdPartyServices:EnableGeolocation=true
- ThirdPartyServices:EnableDistributedCache=true
- GoogleMapsOptions:ApiKey=<<GOOGLE_MAPS_API_KEY>>
- PhotoDownloadScheduledJobOptions:Enabled=true
- PhotoDownloadScheduledJobOptions:DayOfWeek=0
- PhotoDownloadScheduledJobOptions:Hour=12
- PhotoDownloadScheduledJobOptions:Minute=3
- PhotoDownloadScheduledJobOptions:TimeZoneId=America/Chicago
- ConnectionStrings:Redis=redis.slideshow:6379,abortConnect=false,connectTimeout=10000
volumes:
- ./.slides:/app/slides
- ./.logs:/app/logs
depends_on:
redis.slideshow:
condition: service_healthy
redis.slideshow:
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 5s
timeout: 3s
retries: 5
command: redis-server --appendonly yes
volumes:
- ./.redis-data:/dataVolumes are optional, but I find them useful to be able to access the downloaded photos and logs.
Refer to Photo Location and Redis for geolocation and caching, including getting the Google Maps API.
Build and run:
docker-compose -f docker-compose.yaml -f docker-compose.local.yaml build
docker-compose -f docker-compose.yaml -f docker-compose.local.yaml up -dNote: It would be a good idea to rename the image in both Docker compose files and remove my name from the image name.
Two options:
- Download the latest image from my Docker Hub Repo: esausilva/synology.photos.slideshow.api.
- Build the image yourself and push it to your own Docker Hub repository. Following this route, you will need to rename the image to match your repository in the
docker-compose.ymlfile.
For option 2:
Build and push to Docker Hub:
docker-compose build
docker push [your-repo]/synology.photos.slideshow.api:latestThis will take the default docker compose file, docker-compose.yaml, and build the image, skipping the local docker compose file, docker-compose.local.yml.
For both options:
From Synology Container Manager, click on the "Registry" tab and search for the appropriate repository and image.
Right-click on the image and select "Download this image".
Once the image is downloaded, you can create a container from it by going to the "Image" tab, then right-clicking on the image, and selecting "Run".
From there, you can configure the container. In the first screen you will need to set the container name, I would suggest checking-off the "Enable auto-restart" option.
On the second screen, configure the local (to the NAS) ports. You can choose to use the default ports, or you can change them to whatever you want. Just be mindful that the API endpoint ports will need to match the ports you configure here.
Setting up volumes is optional, but I find them useful to be able to access the downloaded photos and logs. You will need to create the folders at your desired location in the NAS with File Station, then map them to the container by clicking the "Add Folder" button under the "Volume Settings" heading.
The volume maps in the container will be /app/slides and /app/logs, make sure you assign Read/Write permissions to the volumes.
Finally, you need to configure the environment variables under the "Environment" heading.
The environment variables will be as follows:
| Environment Variable | Value |
|---|---|
| ASPNETCORE_URLS | http://+:5097 |
| UriBase:ServerIpOrHostname | [[SERVER_IP]] |
| UriBase:Port | 5000 |
| SynologyUser:Account | [[ACCOUNT]] |
| SynologyUser:Password | [[PASSWORD]] |
| SynoApiOptions:FileStationSearchFolders:0 | [[PATH_TO_PHOTOS_FOLDER_IN_NAS]] |
| SynoApiOptions:NumberOfPhotoDownloads | 79 |
| SynoApiOptions:DownloadAbsolutePath | /app/slides |
| ThirdPartyServices:EnableGeolocation | true |
| ThirdPartyServices:EnableDistributedCache | true |
| GoogleMapsOptions:ApiKey | [[GOOGLE_MAPS_API_KEY]] |
| PhotoDownloadScheduledJobOptions:Enabled | true |
| PhotoDownloadScheduledJobOptions:DayOfWeek | 0 |
| PhotoDownloadScheduledJobOptions:Hour | 12 |
| PhotoDownloadScheduledJobOptions:Minute | 3 |
| PhotoDownloadScheduledJobOptions:TimeZoneId | America/Chicago |
| ConnectionStrings:Redis | [[SERVER_IP]]:6379,abortConnect=false,connectTimeout=10000 |
Refer to Photo Location and Redis for geolocation and caching, including getting the Google Maps API.
I highly suggest you create a DHCP reservation in your router for the IP address of your Synology NAS device.
This will make the IP predictable and not change every time your NAS restarts, or DHCP assigns a new IP address.
I would like to add the following features (in no particular order):
| Feature | Description | Status |
|---|---|---|
| Scheduled Jobs | Automates downloading new photo sets in the background at set intervals. | ✅ |
| Real-time Notifications | Uses SignalR or SSE to notify the client when new photos are available. A predecessor to this is to have the background job feature completed. | ✅ |
| Permanent Folder | A dedicated folder for specific photos (e.g., recent trips) that bypasses the auto-clean process. | |
| Delete Endpoint | Allows removing specific photos from the slideshow cache without deleting the original NAS files. | ✅ |
| Metadata Refactoring | Updates endpoints to include photo date, location, and mapping data. | ✅ |
| Blacklist System | An endpoint to permanently prevent specific photos from appearing in the slideshow. | |
| Download Configuration | Enables the client application to define how many photos are fetched. |
What else? Will see...
The web client app is available at: Synology Photos Slideshow Client
I am using my own Synology API SDK to do the heavy lifting of interacting with the official Synology API to fetch the photos and request the download.
Check it out:
- GitHub Repo: Synology API SDK
- NuGet Package: Synology.API.SDK
If you find this project useful in any way, consider getting me a coffee by clicking on the image below. I would really appreciate it!


