Organize the mess of your photo folders with Node
Tags: node, javascript, photos
Published: 2023-11-21 16:31:00

---
title: Organize the mess of your photo folders with Node
published: true
description: How to organize your photos with a Node script
tags: node, js, photos
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rqs3tdn28bjimlud75mi.png
# https://dev-to-uploads.s3.amazonaws.com/uploads/articles/acuoeb69ofll4297xmxl.png
# Use a ratio of 100:42 for best results.
published_at: 2023-11-21 16:31 +0000
---

If you have trouble organizing your photos spread across several folders like me, <u>this article can help you</u>!

I recently experienced a hard drive crash 💥 containing all of my photos 😢. Luckily, I found backups and recovered some photos from friends and family. But I really had a hard time organizing all these photos. After 4 days of repetitive renaming and moving tasks, I decided to create a script to automate this horribly time-consuming task and **maybe save someone else**.

As you can see it was a bit of a mess 😅
![Screenshot photos before](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/waxl53z6ledeezhmpyst.png)


## Defining an effective organization
> The paragraph below is an opinionated thought. You are free to implement any other organizational system with the code given.

**My first try** was to move all the photos and videos into a **single folder** and let the *"Last Updated Date"* sort it. Simply scroll to the desired date and view photos from that period. But several thousand photos in a single folder took too long to load and scroll through. Moreover, I had too many name conflicts with photos with the same name from different smartphones.

**Second try**: **Divided by year and month**. Few photos in folders, few conflicts. After organizing a few years of photos, I realized that it was painful to read. I was constantly changing folders to view my photos. A really not pleasant experience.

**Third try**: Just **divided by year**. Not too many photos in folders, a significant number of name conflicts. But easy to read without changing folders to watch them. This looks better but there are still quite a few name conflicts to resolve…


👉 **I chose the option: <u>divide by year</u> to easily navigate over time and limit the loading time of each folder**

## Create a Node script to import once
The code below recursively scan folders and copy photos to an output folder divided by year. Enough for a single run, but will override photos on incremental import.

```js
const { resolve } = require('path');
const { readdir } = require('fs').promises;
const path = require('path');
var fs = require('fs')

const inPathdir = "/Users/alagrede/Desktop/inputPhotos";
const outPathdir = "/Users/alagrede/Desktop/photos/";


// recursively read folders
async function* getFiles(dir) {
  const dirents = await readdir(dir, { withFileTypes: true });
  for (const dirent of dirents) {
    const res = resolve(dir, dirent.name);
    if (dirent.isDirectory()) {
      yield* getFiles(res);
    } else {
      yield res;
    }
  }
}

(async () => {
  let totalCount = 0;
  for await (const f of getFiles(inPathdir)) {
    const name = path.parse(f).name;
    const dirname = path.dirname(f);
    const extname = path.extname(f);
    const stats = fs.statSync(f);
    const time = stats.birthtime;
    const year = time.getFullYear();
    const yearDir = outPathdir + year + "/";

    if (!fs.existsSync(yearDir)) {
      fs.mkdirSync(yearDir);
    }

    const dateformatted = time.toISOString().split('T')[0]; // 2012-10-25
    const outFilename = `${yearDir}IMG-${dateformatted}_${count}${extname}`;
    
    fs.copyFileSync(f, outFilename);
    fs.utimesSync(outFilename, time, time);
    totalCount++;
  }

  console.log(`Successfully imported ${totalCount} files`);
})()
```

![Screenshot photos after](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/80ev5tyymlxt6079r2cc.png)

## Improve to import incrementally
Now we do the same thing but this time searching for the last photo number in each year folder to no longer overwrite existing photos.
```js
const { resolve } = require('path');
const { readdir } = require('fs').promises;
const path = require('path');
var fs = require('fs')

const inPathdir = "/Users/alagrede/Desktop/inputPhotos";
const outPathdir = "/Users/alagrede/Desktop/photos/";

// recursively read folders
async function* getFiles(dir) {
  const dirents = await readdir(dir, { withFileTypes: true });
  for (const dirent of dirents) {
    const res = resolve(dir, dirent.name);
    if (dirent.isDirectory()) {
      yield* getFiles(res);
    } else {
      yield res;
    }
  }
}

(async () => {
  
  const sortByNameFunc = function(a,b) {
    const nameA = path.parse(a).name;
    const nameB = path.parse(b).name;
    if (nameA < nameB) return 1;
    if (nameA > nameB) return -1;
    return 0;
  }

  const sortByBirthtimeFunc = function(a,b) {
    return fs.statSync(a).birthtime - fs.statSync(b).birthtime;
  }

  async function getFilesSortedBy(dir, sortFunction) {
    let files = []
      for await (const f of getFiles(dir)) {
        // avoid considering hidden cache files
        if (!path.parse(f).name.startsWith(".")) 
          files.push(f);
      }
      return files.sort(sortFunction);
  }

  async function getMaxCounterInDir(dir) {
    const files = await getFilesSortedBy(dir, sortByNameFunc);
    if (files.length === 0) return 1;
    return path.parse(files[0]).name.split(".")[0].split("_").slice(-1);
  }

  const inFiles = await getFilesSortedBy(inPathdir, sortByBirthtimeFunc);

  const currentCounterByYear = new Map();
  
  let totalCount = 0;
  // Start to copy files to dest
  for await (const f of inFiles) {
    const name = path.parse(f).name;
    const dirname = path.dirname(f);
    const extname = path.extname(f);
    const stats = fs.statSync(f);
    const time = stats.birthtime;
    const year = time.getFullYear();
    const yearDir = outPathdir + year + "/";

    // find the current max counter for year directory
    if (!currentCounterByYear.has(year)) {
      if (!fs.existsSync(yearDir)) {
        fs.mkdirSync(yearDir);
        currentCounterByYear.set(year, 1);
      } else {
        currentCounterByYear.set(year, await getMaxCounterInDir(yearDir));
      }
    }

    // copy file to dest
    const count = currentCounterByYear.get(year);

    const dateformatted = time.toISOString().split('T')[0]; // 2012-10-25
    const outFilename = `${yearDir}IMG-${dateformatted}_${count}${extname}`;
    
    currentCounterByYear.set(year, ++count);
    fs.copyFileSync(f, outFilename);
    fs.utimesSync(outFilename, time, time);
    totalCount++;
  }

  console.log(`Successfully imported ${totalCount} files`);
})()
```

Perfect for importing photos over time! And I’m super happy with this. 
![Perfect image](https://media.giphy.com/media/AyPs520p8Di2k/giphy.gif)

## Just one more thing
I also want to show you how to avoid photo duplication, allowing you to import any folder with existing photos without worrying about it.

## Remove duplicates
To find duplicate photos in bulk, I cross-reference the file **size** with the **updated time**. If these 2 values are exactly the same, I assume they are the same files.

```js
const { resolve } = require('path');
const { readdir } = require('fs').promises;
const path = require('path');
var fs = require('fs')

const pathdir = "/Users/alagrede/Desktop/out/";

async function* getFiles(dir) {
  const dirents = await readdir(dir, { withFileTypes: true });
  for (const dirent of dirents) {
    const res = resolve(dir, dirent.name);
    if (dirent.isDirectory()) {
      yield* getFiles(res);
    } else {
      yield res;
    }
  }
}

(async () => {
  let m = new Map();

  // count files with same updated time and size
  for await (const f of getFiles(pathdir)) {
    const name = path.parse(f).name;
    const dirname = path.dirname(f);
    const extname = path.extname(f);
      if (!name.startsWith("."))
        const stats = fs.statSync(f);
        const length = stats.size;
        const mtime = stats.mtime;
        const timestamp = mtime.getTime();
        const key = `${timestamp}-${length}`
        if (!m.has(key)) {
          m.set(key, []);
        }
        m.get(key).push(f);
      }
  }

  // remove files appearing once
  for (let k of m.keys()) {
    if (m.get(k).length === 1)
      m.delete(k);
  }

  let totalCount = 0;
  for (let k of m.keys()) {
    let files = m.get(k);
    files.shift(); // remove the first file
    // Delete the others
    for (let f of files) {
      fs.unlinkSync(f);
      totalCount++;
    }
  }

  console.log(`Successfully deleted ${totalCount} duplicate files!`)
})()
```

## Work done for today 👍
This system is enough for me to organize all photos and videos and enjoy viewing them without wasting time. I hope these scripts will help you. 

You can find them <a href="https://recipe.znote.io/recipes/organize-your-photos">here</a> directly executable and editable through Znote. (A very simple way to work on this kind of script)

If you struggle with this tutorial, I also package a little app here: <a href="https://photo-organizer.app">photo-organizer.app</a> with advanced features *(a bit long to explain in a single article but you will find below the libraries used)*. Among these features, you will be able to find similar photos (with **pixel comparison**) and **resize images**.
<a href="https://photo-organizer.app"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/31ioqwmiq8as71qkqk74.png" /></a>



Librairies used:
- [blockhash-core](https://github.com/LinusU/blockhash-core)
- [sharp](https://github.com/lovell/sharp)