Skip to content

Commit

Permalink
Allow easy configuration of supported file types and add support for …
Browse files Browse the repository at this point in the history
…HEIC+MOV (#23)
  • Loading branch information
mattwilson1024 authored Oct 9, 2021
1 parent 9d5b099 commit e272b53
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 26 deletions.
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,33 @@ Takeout
...
```

## Supported file types
## Configuring supported file types

This tool currently only extracts the following "media file" types. Any other files will be ignored and not included in the output:
- .jpg
- .jpeg
- .gif
- .png
- .mp4
- .avi
In order to avoid touching files that are not photos or videos, this tool will only process files whose extensions are whitelisted in the configuration options. Any other files will be ignored and not included in the output.

To customise which files are processed, edit the `src/config.ts` file to suit your needs. For each extension you can also configure whether or not to attempt to read/write EXIF metadata for that file type.

The default configuration is as follows:
```
┌──────────┬─────────┐
│Extension │EXIF │
├──────────┼─────────┤
│.jpeg │true │
│.jpg │true │
│.heic │true │
│.gif │false │
│.mp4 │false │
│.png │false │
│.avi │false │
│.mov │false │
└──────────┴─────────┘
```

## What does the tool do?

The tool will do the following:
1. Find all "media files" with one of the supported extensions listed above from the (nested) `inputDir` folder structure.

1. Find all "media files" with one of the supported extensions (see "Configuring supported file types" above) from the (nested) `inputDir` folder structure.
2. For each "media file":

a. Look for a corresponding sidecar JSON metadata file (see the section below for more on this) and if found, read the `photoTakenTime` field
Expand Down
14 changes: 14 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Config } from './models/config-models';

export const CONFIG: Config = {
supportedMediaFileTypes: [
{ extension: '.jpeg', supportsExif: true },
{ extension: '.jpg', supportsExif: true },
{ extension: '.heic', supportsExif: true },
{ extension: '.gif', supportsExif: false },
{ extension: '.mp4', supportsExif: false },
{ extension: '.png', supportsExif: false },
{ extension: '.avi', supportsExif: false },
{ extension: '.mov', supportsExif: false },
],
};
4 changes: 3 additions & 1 deletion src/helpers/does-file-support-exif.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { extname } from 'path';
import { CONFIG } from '../config';

export function doesFileSupportExif(filePath: string): boolean {
const extension = extname(filePath);
return extension.toLowerCase() === '.jpeg' || extension.toLowerCase() === '.jpg';
const mediaFileType = CONFIG.supportedMediaFileTypes.find(fileType => fileType.extension.toLowerCase() === extension.toLowerCase());
return mediaFileType?.supportsExif ?? false;
}
4 changes: 2 additions & 2 deletions src/helpers/find-files-with-extension-recursively.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getAllFilesRecursively } from './get-all-files-recursively';
import { extname } from 'path';
import { getAllFilesRecursively } from './get-all-files-recursively';

export async function findFilesWithExtensionRecursively(dirToSearch: string, extensionsToInclude: string[]): Promise<string[]> {
const allFiles = await getAllFilesRecursively(dirToSearch);
Expand All @@ -10,7 +10,7 @@ export async function findFilesWithExtensionRecursively(dirToSearch: string, ext

const matchingFiles = allFiles.filter(filePath => {
const extension = extname(filePath).toLowerCase();
return extensionsToInclude.map(ext => ext.toLowerCase()).includes(extension);
return extensionsToInclude.map(ext => ext.toLowerCase()).includes(extension.toLowerCase());
});
return matchingFiles;
}
5 changes: 3 additions & 2 deletions src/helpers/find-supported-media-files.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { existsSync } from 'fs';
import { basename, extname, resolve } from 'path';
import { CONFIG } from '../config';
import { MediaFileInfo } from '../models/media-file-info';
import { SUPPORTED_MEDIA_FILE_EXTENSIONS } from '../models/supported-media-file-extensions';
import { doesFileSupportExif } from './does-file-support-exif';
import { findFilesWithExtensionRecursively } from './find-files-with-extension-recursively';
import { generateUniqueOutputFileName } from './generate-unique-output-file-name';
import { getCompanionJsonPathForMediaFile } from './get-companion-json-path-for-media-file';

export async function findSupportedMediaFiles(inputDir: string, outputDir: string): Promise<MediaFileInfo[]> {
const mediaFilePaths = await findFilesWithExtensionRecursively(inputDir, SUPPORTED_MEDIA_FILE_EXTENSIONS);
const supportedMediaFileExtensions = CONFIG.supportedMediaFileTypes.map(fileType => fileType.extension);
const mediaFilePaths = await findFilesWithExtensionRecursively(inputDir, supportedMediaFileExtensions);

const mediaFiles: MediaFileInfo[] = [];
const allUsedOutputFilesLowerCased: string[] = [];
Expand Down
29 changes: 19 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Command, flags } from '@oclif/command';
import * as Parser from '@oclif/parser';
import { existsSync, promises as fspromises } from 'fs';
import { CONFIG } from './config';
import { doesFileHaveExifDate } from './helpers/does-file-have-exif-date';
import { findSupportedMediaFiles } from './helpers/find-supported-media-files';
import { readPhotoTakenTimeFromGoogleJson } from './helpers/read-photo-taken-time-from-google-json';
import { updateExifMetadata } from './helpers/update-exif-metadata';
import { updateFileModificationDate } from './helpers/update-file-modification-date';
import { Directories } from './models/directories'
import { SUPPORTED_MEDIA_FILE_EXTENSIONS } from './models/supported-media-file-extensions';

const { readdir, mkdir, copyFile } = fspromises;

class GooglePhotosExif extends Command {
static description = `Takes in a directory path for an extracted Google Photos Takeout. Extracts all JPEGs, GIFs and MP4 files and places them into an output directory. All files will have their modified timestamp set to match the timestamp specified in Google's JSON metadata files (where present). In addition, for file types that support EXIF, the EXIF "DateTimeOriginal" field will be set to the timestamp from Google's JSON metadata, if the field is not already set in the EXIF metadata.`;
static description = `Takes in a directory path for an extracted Google Photos Takeout. Extracts all photo/video files (based on the conigured list of file extensions) and places them into an output directory. All files will have their modified timestamp set to match the timestamp specified in Google's JSON metadata files (where present). In addition, for file types that support EXIF, the EXIF "DateTimeOriginal" field will be set to the timestamp from Google's JSON metadata, if the field is not already set in the EXIF metadata.`;

static flags = {
version: flags.version({char: 'v'}),
Expand Down Expand Up @@ -94,16 +94,22 @@ class GooglePhotosExif extends Command {
}

private async processMediaFiles(directories: Directories): Promise<void> {
this.log(`--- Finding supported media files (${SUPPORTED_MEDIA_FILE_EXTENSIONS.join(', ')}) ---`)
// Find media files
const supportedMediaFileExtensions = CONFIG.supportedMediaFileTypes.map(fileType => fileType.extension);
this.log(`--- Finding supported media files (${supportedMediaFileExtensions.join(', ')}) ---`)
const mediaFiles = await findSupportedMediaFiles(directories.input, directories.output);

const jpegs = mediaFiles.filter(mediaFile => mediaFile.mediaFileExtension.toLowerCase() === '.jpeg' || mediaFile.mediaFileExtension.toLowerCase() === '.jpg');
const gifs = mediaFiles.filter(mediaFile => mediaFile.mediaFileExtension.toLowerCase() === '.gif');
const mp4s = mediaFiles.filter(mediaFile => mediaFile.mediaFileExtension.toLowerCase() === '.mp4');
const pngs = mediaFiles.filter(mediaFile => mediaFile.mediaFileExtension.toLowerCase() === '.png');
const avis = mediaFiles.filter(mediaFile => mediaFile.mediaFileExtension.toLowerCase() === '.avi');
// Count how many files were found for each supported file extension
const mediaFileCountsByExtension = new Map<string, number>();
supportedMediaFileExtensions.forEach(supportedExtension => {
const count = mediaFiles.filter(mediaFile => mediaFile.mediaFileExtension.toLowerCase() === supportedExtension.toLowerCase()).length;
mediaFileCountsByExtension.set(supportedExtension, count);
});

this.log(`--- Found ${jpegs.length} JPEGs, ${gifs.length} GIFs, ${pngs.length} PNGs, ${mp4s.length} MP4s and ${avis.length} AVIs ---`);
this.log(`--- Scan complete, found: ---`);
mediaFileCountsByExtension.forEach((count, extension) => {
this.log(`${count} files with extension ${extension}`);
});

this.log(`--- Processing media files ---`);
const fileNamesWithEditedExif: string[] = [];
Expand Down Expand Up @@ -132,7 +138,10 @@ class GooglePhotosExif extends Command {
}

// Log a summary
this.log(`--- Processed ${mediaFiles.length} media files (${jpegs.length} JPEGs, ${gifs.length} GIFs, ${pngs.length} PNGs, ${mp4s.length} MP4s and ${avis.length} AVIs) ---`);
this.log(`--- Finished processing media files: ---`);
mediaFileCountsByExtension.forEach((count, extension) => {
this.log(`${count} files with extension ${extension}`);
});
this.log(`--- The file modified timestamp has been updated on all media files ---`)
if (fileNamesWithEditedExif.length > 0) {
this.log(`--- Found ${fileNamesWithEditedExif.length} files which support EXIF, but had no DateTimeOriginal field. For each of the following files, the DateTimeOriginalField has been updated using the date found in the JSON metadata: ---`);
Expand Down
8 changes: 8 additions & 0 deletions src/models/config-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Config {
supportedMediaFileTypes: IMediaFileType[];
}

export interface IMediaFileType {
extension: string;
supportsExif: boolean;
}
1 change: 0 additions & 1 deletion src/models/supported-media-file-extensions.ts

This file was deleted.

1 comment on commit e272b53

@ddirector51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This worked great for me. Thanks so much! Recursion would be nice, but that's just me being picky.

Please sign in to comment.