From 2ecbe281b0351e630c7730b44c1f4a9c3b3062a5 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Sun, 26 Aug 2018 21:22:32 -0400 Subject: Initial commit --- src/Commands/ExtractCommand.cs | 158 +++++++++++++++++++++++++++++++++++++++++ src/Commands/ICommand.cs | 7 ++ src/Commands/PreviewCommand.cs | 149 ++++++++++++++++++++++++++++++++++++++ src/Commands/RootCommand.cs | 34 +++++++++ 4 files changed, 348 insertions(+) create mode 100644 src/Commands/ExtractCommand.cs create mode 100644 src/Commands/ICommand.cs create mode 100644 src/Commands/PreviewCommand.cs create mode 100644 src/Commands/RootCommand.cs (limited to 'src/Commands') diff --git a/src/Commands/ExtractCommand.cs b/src/Commands/ExtractCommand.cs new file mode 100644 index 0000000..5872fd6 --- /dev/null +++ b/src/Commands/ExtractCommand.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.CommandLineUtils; + +namespace iPhotoExtractor.Commands +{ + public class ExtractCommand : ICommand + { + public static void Configure(CommandLineApplication command) + { + command.HelpOption("-h|--help"); + command.Description = "Copy photos from their original locations to a directory " + + "structure mirroring the event albums within iPhoto."; + + var libPathArgument = command.Argument( + "library_dir", + "Path to the iPhoto library directory."); + + var outputDirArgument = command.Argument( + "[output_dir]", + "Path to the directory where the extracted photos will be copied."); + + command.OnExecute(() => + { + (new ExtractCommand(libPathArgument.Value, outputDirArgument.Value)).Run(); + return 0; + }); + } + + private readonly string _libraryDir; + private readonly string _outputDir; + + public ExtractCommand(string libraryDir, string outputDir) + { + _libraryDir = libraryDir; + _outputDir = outputDir; + } + + public void Run() + { + var libraryDir = String.IsNullOrWhiteSpace(_libraryDir) ? + Directory.GetCurrentDirectory() : + _libraryDir; + + var outputDir = String.IsNullOrWhiteSpace(_outputDir) ? + Path.Combine(Directory.GetCurrentDirectory(), "Extracted Photos") : + _outputDir; + + var dbPath = Path.Combine(libraryDir, "iPhotoMain.db"); + + if (!File.Exists(dbPath)) + { + Console.WriteLine($"File '{dbPath}' not found."); + return; + } + + if (Directory.Exists(outputDir)) + { + Console.Write($"Warning: directory '{outputDir}' already exists. Continue (y/n)? "); + var response = Console.ReadLine().Trim().ToLower(); + + if (response != "y" && response != "yes") + return; + } + + Directory.CreateDirectory(outputDir); + + var photoStore = new PhotoStore(dbPath); + List photos = photoStore.GetAllPhotos(); + + Dictionary> albums = photos + .GroupBy(p => p.AlbumName) + .ToDictionary(g => g.Key, g => g.ToList()); + + Console.WriteLine($"Found {albums.Keys.Count} albums."); + var counter = 1; + + foreach (string album in albums.Keys) + { + var albumPhotos = albums[album]; + var s = albumPhotos.Count == 1 ? "" : "s"; + + Console.WriteLine($"[{counter}/{albums.Keys.Count}] Extracting album '{album}' " + + $"({albumPhotos.Count} photo{s})..."); + + ExtractAlbum(libraryDir, outputDir, album, albumPhotos); + counter++; + } + + Console.WriteLine("Done."); + } + + private void ExtractAlbum( + string libraryDir, + string outputDir, + string albumName, + List photos) + { + string albumDir = String.IsNullOrWhiteSpace(albumName) ? + Path.Combine(outputDir, "Untitled Album") : + Path.Combine(outputDir, albumName); + + if (!Directory.Exists(albumDir)) + Directory.CreateDirectory(albumDir); + + var hasModified = photos.Any(p => p.HasModifiedVersion()); + string originalsDir = Path.Combine(albumDir, "Originals"); + + if (hasModified && !Directory.Exists(originalsDir)) + Directory.CreateDirectory(originalsDir); + + foreach (var photo in photos) + { + List paths = photo.GetUniquePaths(PhotoPathType.Modified); + bool isModified = false; + + if (paths.Any()) + { + isModified = true; + + foreach (string path in paths) + { + CopyPhoto(libraryDir, albumDir, path); + } + } + + paths = photo.GetUniquePaths(PhotoPathType.Original); + string destDir = isModified ? originalsDir : albumDir; + + foreach (string path in paths) + { + CopyPhoto(libraryDir, destDir, path); + } + } + } + + private void CopyPhoto(string libraryDir, string destDir, string relativePhotoPath) + { + string fileName = Path.GetFileNameWithoutExtension(relativePhotoPath); + string extension = Path.GetExtension(relativePhotoPath); + string destFileName = $"{fileName}{extension}"; + var suffix = 2; + + while (File.Exists(Path.Combine(destDir, destFileName))) + { + destFileName = $"{fileName} ({suffix}){extension}"; + suffix++; + } + + string sourcePath = Path.Combine(libraryDir, relativePhotoPath); + string destPath = Path.Combine(destDir, destFileName); + + File.Copy(sourcePath, destPath); + } + } +} \ No newline at end of file diff --git a/src/Commands/ICommand.cs b/src/Commands/ICommand.cs new file mode 100644 index 0000000..8515d48 --- /dev/null +++ b/src/Commands/ICommand.cs @@ -0,0 +1,7 @@ +namespace iPhotoExtractor.Commands +{ + public interface ICommand + { + void Run(); + } +} \ No newline at end of file diff --git a/src/Commands/PreviewCommand.cs b/src/Commands/PreviewCommand.cs new file mode 100644 index 0000000..aea4afc --- /dev/null +++ b/src/Commands/PreviewCommand.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.CommandLineUtils; + +namespace iPhotoExtractor.Commands +{ + public class PreviewCommand : ICommand + { + private const char TEE = '\u251c'; + private const char ELBOW = '\u2514'; + private const char V_BAR = '\u2502'; + private const char H_BAR = '\u2500'; + + public static void Configure(CommandLineApplication command) + { + command.HelpOption("-h|--help"); + command.Description = "Display the directory structure that would result from " + + "running 'iPhotoExtractor extract'."; + + var libPathArgument = command.Argument( + "library_dir", + "Path to the iPhoto library directory."); + + var outputDirArgument = command.Argument( + "[output_dir]", + "Path to the directory where extracted photos will be copied."); + + command.OnExecute(() => + { + (new PreviewCommand(libPathArgument.Value, outputDirArgument.Value)).Run(); + return 0; + }); + } + + private readonly string _libraryDir; + private readonly string _outputDir; + + public PreviewCommand(string libraryDir, string outputDir) + { + _libraryDir = libraryDir; + _outputDir = outputDir; + } + + public void Run() + { + var libraryDir = String.IsNullOrWhiteSpace(_libraryDir) ? + Directory.GetCurrentDirectory() : + _libraryDir; + + var outputDir = String.IsNullOrWhiteSpace(_outputDir) ? + Directory.GetCurrentDirectory() : + _outputDir; + + var dbPath = Path.Combine(libraryDir, "iPhotoMain.db"); + + if (!File.Exists(dbPath)) + { + Console.WriteLine($"File '{dbPath}' not found."); + return; + } + + var photoStore = new PhotoStore(dbPath); + List photos = photoStore.GetAllPhotos(); + + Dictionary> albums = photos + .GroupBy(p => p.AlbumName) + .ToDictionary(g => g.Key, g => g.ToList()); + + Console.WriteLine(_outputDir); + string album; + + for (int i = 0; i < albums.Keys.Count - 1; i++) + { + album = albums.Keys.ElementAt(i); + DrawAlbumTree(album, albums[album], false); + } + + album = albums.Keys.Last(); + DrawAlbumTree(album, albums[album], true); + } + + private void DrawAlbumTree(string albumName, List photos, bool isLastChild) + { + var modified = photos + .Where(p => p.HasModifiedVersion() && p.HasOriginalVersion()) + .ToList(); + + var hasModified = modified.Any(); + string prefix = isLastChild ? " " : $"{V_BAR} "; + + Console.WriteLine($"{GetCurrLevelTreeString(isLastChild)}{albumName}"); + DrawPhotos(photos, prefix, hasModified); + + if (!hasModified) + return; + + Console.WriteLine($"{prefix}{GetCurrLevelTreeString(true)}Originals"); + DrawPhotos(modified, prefix + " ", false); + } + + private void DrawPhotos(List photos, string prefix, bool hasModified) + { + for (int i = 0; i < photos.Count; i++) + { + var photo = photos.ElementAt(i); + var isLastChild = i == photos.Count - 1 && !hasModified; + + List paths = new List(); + + if (hasModified) + paths.AddRange(photo.GetUniquePaths(PhotoPathType.Modified)); + + if (!paths.Any()) + paths.AddRange(photo.GetUniquePaths(PhotoPathType.Original)); + + if (!paths.Any()) + paths.Add(""); + + for (int j = 0; j < paths.Count; j++) + { + var path = paths.ElementAt(j); + + if (j < paths.Count - 1) + DrawPhoto(path, prefix, false); + else + DrawPhoto(path, prefix, isLastChild); + } + } + } + + private void DrawPhoto(string relativePath, string prefix, bool isLastChild) + { + string fileName = Path.GetFileName(relativePath); + string currLevel = GetCurrLevelTreeString(isLastChild); + + Console.WriteLine($"{prefix}{currLevel}{fileName} ({relativePath})"); + } + + private string GetCurrLevelTreeString(bool isLastChild) + { + if (isLastChild) + return $"{ELBOW}{H_BAR}{H_BAR} "; + else + return $"{TEE}{H_BAR}{H_BAR} "; + } + } +} \ No newline at end of file diff --git a/src/Commands/RootCommand.cs b/src/Commands/RootCommand.cs new file mode 100644 index 0000000..1abab5a --- /dev/null +++ b/src/Commands/RootCommand.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.CommandLineUtils; + +namespace iPhotoExtractor.Commands +{ + public class RootCommand : ICommand + { + public static void Configure(CommandLineApplication app) + { + app.Name = "iPhotoExtractor"; + app.HelpOption("-h|--help"); + + app.Command("preview", PreviewCommand.Configure); + app.Command("extract", ExtractCommand.Configure); + + app.OnExecute(() => + { + (new RootCommand(app)).Run(); + return 0; + }); + } + + private readonly CommandLineApplication _app; + + public RootCommand(CommandLineApplication app) + { + _app = app; + } + + public void Run() + { + _app.ShowHelp(); + } + } +} \ No newline at end of file -- cgit v1.2.3