📦 Nakatori Labs

ImageBundle

Professional photo distribution & IPTC automation platform. Upload once, route intelligently, deliver everywhere.

🌐

What is ImageBundle?

ImageBundle is a server-side platform for sports and news photographers to automate photo distribution. You upload images once via FTP, and the system automatically reads the embedded IPTC/XMP metadata, evaluates your routing rules, rewrites metadata where needed, and delivers images to multiple FTP destinations — all without manual intervention.

Typical use cases:

  • Route images of Athlete A to their agency FTP, while images of Athlete B go to a different one — simultaneously
  • Strip a credit line from descriptions before sending to Shutterstock
  • Send all images from an event to 5 general outlets, plus one athlete-specific outlet per image
  • Manage multiple independent client workflows (Bundles) from a single server
🧩

Core Concepts

📦
Bundle
A self-contained workflow. Has its own upload folder, destinations, and rules. Think of it as a "project" or "client".
📡
Destination
An FTP server where images are delivered. Each Bundle can have multiple destinations.
⚙️
Rules
Logic that decides which image goes to which destination, based on metadata like athlete name or keywords.
🏷️
IPTC / XMP
Metadata embedded in JPEG files. Fields like caption, keywords, and "Persons Shown" drive routing decisions.
✏️
Transforms
Rewrite IPTC fields before delivery — remove text, replace strings, set new values per destination.
📋
Jobs
Each upload triggers a Job. You can track processing status and per-destination distribution results.
🔄

How it Works

Every image upload goes through this pipeline:

1
FTP Upload
2
Bundle Detection
3
IPTC / XMP Read
4
Rules Evaluated
5
IPTC Rewrite
6
Delivered via FTP
  1. Upload: You drop a JPEG into the bundle_<id> folder via FTP.
  2. Bundle Detection: The FTP handler reads the folder name and matches it to a Bundle in the database.
  3. Metadata Read: The IPTC engine extracts embedded IPTC-IIM fields and XMP fields (including "Persons Shown" written by Photo Mechanic).
  4. Rules Evaluated: The rules engine checks metadata against the bundle's routing rules to determine which destinations should receive the image.
  5. IPTC Rewrite: Per-destination transforms are applied (e.g., strip copyright text from caption for Shutterstock).
  6. Delivery: The image (with rewritten metadata) is uploaded to each matched destination FTP server via a background Celery worker.
🔑

Logging In

All configuration is done via the REST API. The easiest way is through the Swagger UI at /docs.

1
Open the API docs

Navigate to https://imagebundle.nakatori.agency/docs

2
Call POST /api/auth/login

Expand the endpoint, click "Try it out", enter your username and password, then Execute.

3
Copy the access_token

From the response body, copy the value of access_token (the long string starting with eyJ…).

4
Authorize

Click the Authorize button at the top of /docs, paste the token in the Value field, and click Authorize. You're in.

💡

Tokens expire after 30 minutes. If requests start returning 401, just repeat the login flow to get a fresh token.

📦

Bundles

A Bundle is the top-level container for a workflow. Create one per client, event type, or distribution channel.

Creating a Bundle

Use POST /api/bundles with:

{
  "name": "Orange Pictures — General",
  "description": "Main distribution bundle for all events"
}

The response includes the bundle's id. Note it down — you'll need it for the upload folder name.

Upload Folder

After creating a bundle with id 3, create a subfolder named bundle_3 in your FTP home directory. Any JPEG you upload there will be processed by Bundle 3's rules.

ℹ️

The folder must be named exactly bundle_<id> — e.g. bundle_1, bundle_3, bundle_12. The system matches on this pattern.

Bundle Rules

Rules are stored as JSON inside the bundle. Set them via PUT /api/bundles/{id} with a rules field. See the Rules Engine section for the full schema.

📡

Destinations

A Destination is an FTP server that images are delivered to. Each destination belongs to a Bundle.

Creating a Destination

Use POST /api/destinations:

{
  "bundle_id": 1,
  "name": "Getty Images FTP",
  "destination_type": "ftp",
  "host": "ftp.gettyimages.com",
  "port": 21,
  "username": "your-username",
  "password": "your-password",
  "remote_path": "/uploads"
}

The response includes the destination's id — you'll reference this in your rules.

Testing a Destination

Use POST /api/destinations/{id}/test to verify connectivity before going live. The server will attempt an FTP connection and report success or failure.

⚠️

Passwords are stored encrypted (AES-256 Fernet). They are never returned in plain text after creation.

📤

FTP Upload

ImageBundle runs its own FTP server. Connect with your FTP credentials and upload images into the correct bundle subfolder.

Connection Details

SettingValue
Hostimagebundle.nakatori.agency
Port21
ModePassive (PASV)
Usernameyour FTP username (e.g. admin_ftp)
Passwordyour FTP password

Folder Structure

After connecting, you will land in your home directory. Create subfolders named after your bundle IDs:

# Your FTP home directory
/
├── bundle_1/     # → processed by Bundle 1
├── bundle_2/     # → processed by Bundle 2
└── bundle_6/     # → processed by Bundle 6 (test)

Upload JPEG files into the appropriate folder. Processing starts automatically within a few seconds. Only JPEG files are processed; other formats are ignored.

What Happens Next

After upload, a background job is created. You can monitor it via GET /api/jobs. Each job shows the processing status and a list of distributions (one per matched destination) with their delivery status.

⚙️

Rules Overview

Rules are stored as a JSON object on the Bundle. They tell ImageBundle what to do with each uploaded image. Rules are set via PUT /api/bundles/{id} with a rules field.

The top-level structure:

{
  "version": 1,
  "rules": [ /* array of rule objects */ ],
  "destination_transforms": { /* optional, per-destination rewrites */ }
}
📐

Rule Types

distribute_all

Send every image to one or more destinations, unconditionally.

{
  "type": "distribute_all",
  "destination_ids": [1, 2, 3]
}

filtered_distribute

Send images only when metadata matches a condition. Supports multiple filters, each with their own destination list.

{
  "type": "filtered_distribute",
  "filters": [
    {
      "field": "persons_shown",
      "operator": "contains",
      "value": "Femke Kok",
      "destination_ids": [4]
    }
  ]
}
💡

Combine both types in the rules array: a distribute_all rule sends to your 5 general destinations, and a filtered_distribute rule adds an athlete-specific destination on top.

Full example: athlete routing

{
  "version": 1,
  "rules": [
    {
      "type": "distribute_all",
      "destination_ids": [1, 2, 3, 4, 5]  // always send to all 5
    },
    {
      "type": "filtered_distribute",
      "filters": [
        {
          "field": "persons_shown",
          "operator": "contains",
          "value": "Athlete One",
          "destination_ids": [6]   // also send to Athlete One's agency
        },
        {
          "field": "persons_shown",
          "operator": "contains",
          "value": "Athlete Two",
          "destination_ids": [7]   // also send to Athlete Two's agency
        }
      ]
    }
  ]
}
🏷️

Metadata Fields

These are the field names to use in rule filters. They are read from the JPEG's embedded IPTC-IIM data and XMP block.

Field NameSourceDescriptionType
persons_shownXMP (Photo Mechanic)"Persons Shown" — athlete/person nameslist
keywordsIPTC tag 25Keywords / tagslist
captionIPTC tag 120Caption / descriptionstring
headlineIPTC tag 105Headlinestring
creditIPTC tag 110Credit linestring
bylineIPTC tag 80Photographer namestring
cityIPTC tag 90Citystring
countryIPTC tag 101Country namestring
eventXMP (Photo Mechanic)Event namestring
categoryIPTC tag 15Category codestring
ℹ️

persons_shown is written by Photo Mechanic into the XMP block of the JPEG (not the IPTC-IIM block). ImageBundle reads both, so this field works seamlessly.

🔍

Operators

OperatorWorks onBehaviour
containsstring, listValue is a substring of the field, or a substring of any list item
not_containsstring, listInverse of contains
equalsstringExact match (case-sensitive)
not_equalsstringField does not exactly equal the value
starts_withstringField starts with the value
ends_withstringField ends with the value
regexstringField matches the regular expression
existsanyField is present and non-empty
not_existsanyField is absent or empty
✏️

IPTC Transforms

Transforms let you rewrite metadata before an image is delivered to a specific destination. They are defined in the destination_transforms key at the top level of the bundle's rules JSON.

{
  "version": 1,
  "rules": [ /* ... */ ],
  "destination_transforms": {
    "8": [   // destination id 8 (e.g. Shutterstock)
      {
        "field": "caption",
        "operation": "regex_replace",
        "pattern": "\\s*\\(Photo by [^)]+\\)",
        "replacement": ""
      }
    ]
  }
}

Available transform operations:

OperationParametersDescription
setvalueReplace the entire field with a fixed value
prefixvaluePrepend text to the field
suffixvalueAppend text to the field
replacefind, replacementFind and replace a literal string
regex_replacepattern, replacementFind and replace using a regular expression
removevalueRemove an exact value from a list field
remove_patternpatternRemove any list items matching a regex pattern
addvalueAdd a value to a list field (e.g. add a keyword)
💡

Transforms apply only to the copy sent to that destination. The original file on disk is never modified, and other destinations receive the unmodified metadata.

🔀

Multi-destination Routing

ImageBundle supports fully flexible multi-destination routing. A single image can be sent to any combination of destinations simultaneously, with different metadata rewrites per destination.

Scenarios

ScenarioHow to configure
All images → 5 general FTPsdistribute_all with 5 destination_ids
Athlete A → 5 general + FTP 6distribute_all + filtered_distribute on persons_shown
Athlete A → FTP Y only, Athlete B → FTP Z onlyTwo filtered_distribute rules, no distribute_all
Remove credit line for Shutterstockdestination_transforms with regex_replace on caption
Different caption per agencyMultiple entries in destination_transforms

Split routing example

Send Athlete A only to FTP Y, and Athlete B only to FTP Z (completely separate):

{
  "version": 1,
  "rules": [
    {
      "type": "filtered_distribute",
      "filters": [
        {
          "field": "persons_shown",
          "operator": "contains",
          "value": "Athlete A",
          "destination_ids": [10]
        },
        {
          "field": "persons_shown",
          "operator": "contains",
          "value": "Athlete B",
          "destination_ids": [11]
        }
      ]
    }
  ]
}
🔒

FTP Credentials

FTP credentials are managed per user account. Each user gets a dedicated FTP username and password, independent of their API login.

To view or update credentials, use the Users API:

  • GET /api/users — list all users
  • PUT /api/users/{id} — update a user's FTP password
ℹ️

If you forget your password, an admin can reset it server-side using scripts/reset_password.py <username> <new_password>.

📖

API Reference

Full interactive documentation is at /docs. Quick reference:

Authentication

MethodPathDescription
POST/api/auth/loginGet access token
GET/api/auth/meCurrent user info

Bundles

MethodPathDescription
GET/api/bundlesList all bundles
POST/api/bundlesCreate a bundle
GET/api/bundles/{id}Get bundle details + rules
PUT/api/bundles/{id}Update bundle (including rules)
DELETE/api/bundles/{id}Delete a bundle

Destinations

MethodPathDescription
GET/api/destinationsList destinations (filter by ?bundle_id=)
POST/api/destinationsCreate a destination
GET/api/destinations/{id}Get destination details
PUT/api/destinations/{id}Update destination
POST/api/destinations/{id}/testTest FTP connectivity
DELETE/api/destinations/{id}Delete destination

Jobs

MethodPathDescription
GET/api/jobsList jobs (filter by ?status=)
GET/api/jobs/{id}Get job details + distributions

Users

MethodPathDescription
GET/api/usersList users
POST/api/usersCreate user
PUT/api/users/{id}Update user / reset FTP password
DELETE/api/users/{id}Delete user