Skip to content

Instantly share code, notes, and snippets.

@BrunoBernardino
Last active October 3, 2021 11:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BrunoBernardino/4749c955c8814659c6a60f3141b7905b to your computer and use it in GitHub Desktop.
Save BrunoBernardino/4749c955c8814659c6a60f3141b7905b to your computer and use it in GitHub Desktop.
Format + Convert iCloud Photos Export (originals, with moments subfolder)

I wrote these because the apps and suggestions I tried before lacked either live photos, or the YYYY/MM file structure I wanted to organize my photos.

This requires:

  • Homebrew (or, in Linux, if you're running the conversion there, you need imagemagick 7+, cd Downloads && sudo apt-get install libjpeg-dev libtiff-dev libwebp-dev libheif-dev && wget http://www.imagemagick.org/download/ImageMagick.tar.gz && tar -xzvf ImageMagick.tar.gz && cd ImageMagick-* && ./configure --with-heic=yes --with-webp=yes --with-jpg=yes --with-png=yes && make && sudo make install && sudo ldconfig /usr/local/lib)
  • Node v14+
  • The two files here (I put them in ~/Downloads/)
  • Exporting your iCloud Photos originals, with the moments subfolder. I exported them all to ~/Downloads/iCloud Photos Export.

After having your export:

  1. Run npm install moment. Feel free to remove node_modules, package.json, and the package-lock.json files after you finish #3.
  2. Run node export-formatter.js once (make sure you update line 6 with your directory, if you didn't place the files like I mentioned above), it should be pretty quick (I had ~11k files, ~50GB).
  3. Run node export-converter.js once if you want to convert your MOV files to MP4 and your HEIC files to JPG. This one will be slow because it converts movie by movie and image by image, synchronally and sequentially. I did this intentionally, even though it's slower (NOTE: This will delete the original files and just keep the converted ones).

You'll end with your photos exported nicely to a structure of YYYY/MM/original-file-name.jpg, including Live Photos (still + video), all in jpg and mp4 formats.

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const originalExportPath = path.join('./', 'iCloud Photos Export');
const yearDirectories = fs.readdirSync(originalExportPath, { encoding: 'utf-8' });
for (const yearDirectory of yearDirectories) {
const yearStat = fs.statSync(path.join(originalExportPath, yearDirectory));
if (!yearStat.isDirectory()) {
console.log('==== FOUND A NON-DIRECTORY!!!', path.join(originalExportPath, yearDirectory));
continue;
}
const monthDirectories = fs.readdirSync(path.join(originalExportPath, yearDirectory));
for (const monthDirectory of monthDirectories) {
const monthStat = fs.statSync(path.join(originalExportPath, yearDirectory, monthDirectory));
if (!monthStat.isDirectory()) {
console.log('==== FOUND A NON-DIRECTORY!!!', path.join(originalExportPath, yearDirectory, monthDirectory));
continue;
}
const files = fs.readdirSync(path.join(originalExportPath, yearDirectory, monthDirectory));
for (const file of files) {
const filePath = path.join(originalExportPath, yearDirectory, monthDirectory, file);
const stat = fs.statSync(filePath);
if (!stat.isFile()) {
console.log('==== FOUND A NON-FILE!!!', filePath);
continue;
}
let wasConverted = false;
// Convert heic to jpg: magick mogrify -monitor -format jpg {file.heic}
if (path.extname(file).toLowerCase() === '.heic') {
console.log('-- converting HEIC', filePath);
execSync(`magick mogrify -monitor -format jpg "${filePath}"`);
wasConverted = true;
}
// Convert mov to mp4: ffmpeg -i {file.mov} {file.mp4}
if (path.extname(file).toLowerCase() === '.mov') {
console.log('-- converting MOV', filePath);
const newFilePath = filePath.replace(/\.mov$/i, '.mp4')
execSync(`ffmpeg -i "${filePath}" "${newFilePath}"`);
wasConverted = true;
}
if (wasConverted) {
fs.rmSync(filePath);
}
}
}
}
const fs = require('fs');
const path = require('path');
const moment = require('moment');
// NOTE: Change your directory/path in the line below
const originalExportPath = path.join('./', 'iCloud Photos Export');
const directories = fs.readdirSync(originalExportPath, { encoding: 'utf-8' });
for (const directory of directories) {
const files = fs.readdirSync(path.join(originalExportPath, directory));
// Split "moment, day month year" into "year/month", knowing "moment" doesn't always exist
const directoryFullDate = directory.split(',').splice(-1)[0].trim();
const directoryDate = moment(directoryFullDate, 'D MMMM YYYY');
if (!directoryDate.isValid()) {
console.log('==== FOUND AN INVALID DIRECTORY!!!', directory);
continue;
}
for (const file of files) {
const stat = fs.statSync(path.join(originalExportPath, directory, file));
if (!stat.isFile()) {
console.log('==== FOUND A NON-FILE!!!', path.join(originalExportPath, directory, file));
continue;
}
const newDirectoryPath = path.join(originalExportPath, directoryDate.format('YYYY/MM'));
if (!fs.existsSync(newDirectoryPath)) {
console.log('-- Creating directory...', directoryDate.format('YYYY/MM'));
fs.mkdirSync(newDirectoryPath, { recursive: true });
}
const newFinalFilePath = path.join(newDirectoryPath, file);
fs.renameSync(path.join(originalExportPath, directory, file), newFinalFilePath);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment