Automate it!

It’s sound human nature to avoid doing repetitive tasks. jAlbum was once developed as I didn’t want to manually prepare 120 images for the web and create the needed HTML. Even if jAlbum has a good number of batch processing features, you may find yourself doing manual repetitive work, for instance deleting 1-star images, looking for broken links, renaming files, changing comments, or looking for certain images (panoramas for instance). If there is ANY pattern to the work you’re doing, then you can automate it using jAlbum’s flexible scripting environment. Your aim might not even be to create a web gallery but simply to give your images more descriptive names than “IMG_6624.JPG”. How about letting jAlbum rename such images in the format cameraModel+location for instance? I mean, a file called “iPhone SE-New York-7.JPG” tells me A LOT more than “IMG_6624.JPG”, don’t you agree?

With jAlbum’s scripting environment (Tools->System console), you can automate such tasks with only a few lines of code, preferably in the easy-to-learn Groovy language or JavaScript. Long-running tasks will automatically pop up a progress indicator allowing you to abort the task, and most operations (not deletions) can also be undone in one go if you change your mind.

Without further ado, let’s go through some examples (Nothing beats learning by example). The following scripts operate on all items of the currently opened project, but they can easily be modified to operate on the current folder only, or selected items/objects only (see further down). To try these scripts, copy and paste them to the upper panel of jAlbum’s system console (one at a time), then hit the Execute button (CMD/CTRL+E) to execute them (you can also select a part of a script to execute and hit Execute). The result is printed to the lower panel.

Here are some examples to glance at, explanation follows:

Sample scripts (Groovy language, requires jAlbum 22)

// Exclude 1- and 2- star images
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    if (ao.rating == 1 || ao.rating == 2) {
        ao.included = false;
        println ao
    }
}
// Change 4-star images to 5 stars
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    if (ao.rating == 4) {
        ao.rating = 5
        println ao
    }
}
// Exclude videos
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    if (ao.category == Category.video) {
        ao.included = false;
        println ao
    }
}
// Find broken links
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    if (!ao.file.exists()) {
        println ao.file
    }
}

I hope you find this fairly intuitive. Let’s break it down:

  • Work is the Java class that’s responsible for most of the “plumbing” in these scripts (boring common code): It handles the processing of multiple objects (of any kind actually) and can even process multiple objects simultaneously. In case processing takes time, it pops up an abortable progress dialog. The executed operations are grouped in “undo groups” so it’s enough with one (or a few) undo operations to undo all operations. Finally, “Work” makes it easy to prompt the user before the action starts and report the results
  • “.on()” – This method tells Work what objects to work on. It can be either an array, List, or Stream of objects (i.e. images, videos, etc). Typical arguments are as follows:
    • rootFolder.descendants – All objects in the current project
    • currentFolder.children – All objects in the current folder
    • currentFolder.descendants – All objects under the current folder (i.e. including subfolders)
    • selectedObjects – The selected objects
    • TreeCollection.of(selectedObjects) – Selected objects AND their descendants (subfolders)

Note: Excluded objects are skipped when referring to “descendants”. To also include these, refer to the TreeCollection API.

  • forEach(ao) – Code to execute for each encountered object. “ao” is a shorthand for AlbumObject – the API used to interface with objects of a project. Check out the documentation for this API to know what operations you can perform on each object.

Let’s move on with slightly more complex things. Here’s first a script that demonstrates how to pass a filtered stream of objects to Work. In this case 1-star rated objects. This script also prompts the user before proceeding and finally shows a result dialog:

// Delete 1 star images. Prompt user and print result
Work.on(rootFolder.descendants.stream().filter(ao -> ao.rating == 1))
.ask("Ok to delete all 1 star images?")
.forEach(ao) -> {
    ao.delete()
}
.showResult()
// Find links
import se.datadosen.io.LinkFile
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    if (ao.file instanceof LinkFile) {
        println ao.pathFromRoot
    }
}

The following script uses the gotoPath() method to navigate the user to the first object found with a broken link. It also demonstrates how to abort further processing once this first broken link is found:

// Goto first broken link
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    if (!ao.file.exists()) {
        window.albumExplorer.gotoPath(ao.pathFromRoot)
        window.toFront()
        throw new OperationAbortedException()
    }
}
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    if (ao.file.exists() && ao.category == Category.image) {
        println ao.pathFromRoot + ": " + ao.ImageInfo
    }
}
// Find panoramas (i.e. images having a width 2 times the height or more)
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    if (ao.file.exists() && ao.category == Category.image) {
        if (ao.imageInfo.width >= ao.imageInfo.height * 2)
        println ao.pathFromRoot
    }
}
// Replace texts in comments. Look for "Adria" and replace with "Kabe"
Work.on(rootFolder.descendants)
.forEach(ao) -> {
    ao.comment = ao.comment.replaceAll("Adria", "Kabe")
    println ao
}


Here’s a pretty powerful file renaming script: It uses an online location service to map GPS coordinates embedded within images to rename images to the format cameraModel-Place-number. As looking up locations based on GPS coordinates over an external service may be time-consuming, this script uses the parallelStream() call to process objects in parallel, and because of the parallelism, we’re using an AtomicInteger as the number counter to get truly unique numbers for each file:

// Rename files by camera model and place
import java.util.concurrent.atomic.*
counter = new AtomicInteger(1)
Work.on(rootFolder.descendants.parallelStream())
.forEach(ao) -> {
    if (ao.category == Category.image) {
        PlaceService.Place place = PlaceService.instance.getPlace(ao)
        if (place != null) {
            ao.name = ao.vars.cameraModel + "-" + place.name + "-" + counter.getAndIncrement()
        }
    }
}

Finally, here’s a script that is similar to an SQL group-by and order-by, it finds all keywords and presents them by name and number of occurrences, starting with the most common keyword. It uses some pretty powerful Java Stream API calls:

import java.util.stream.*;
import java.util.function.*;

// Present keywords by name and occurrences
result = rootFolder.descendants.stream().map(ao -> ao.keywordSet).flatMap(ks -> ks.stream())
.collect(
 Collectors.groupingBy(
    Function.identity(), Collectors.counting()
  )
);

// Now sort it
result.entrySet().stream().sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.forEachOrdered(e -> println e)

Save scripts as “Tools”

If you’ve made a handy script that you’d like to execute several times, put it into a text file named “preferred name.groovy” and place that file under a “tools” subdirectory of jAlbum’s “config” directory (See Tools → Open directories → Config directory). Now this script can be executed under the Tools → External tools menu (requires a restart to show up). You can find more useful “External tools” under the “tools” folder of jAlbum’s program directory. Inspect these for more inspiration.

I hope this inspires you to investigate the power that lies within jAlbum’s scripting environment. You’ll find more developer help in our developer center. If you get stuck, post questions to our forum. If you make an awesome script you think others would appreciate, share it in our forum.