From 2ecbe281b0351e630c7730b44c1f4a9c3b3062a5 Mon Sep 17 00:00:00 2001
From: Daniel Smith <rdnlsmith@gmail.com>
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<Photo> photos = photoStore.GetAllPhotos();
+
+            Dictionary<string, List<Photo>> 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<Photo> 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<string> 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<Photo> photos = photoStore.GetAllPhotos();
+
+            Dictionary<string, List<Photo>> 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<Photo> 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<Photo> 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<string> paths = new List<string>();
+
+                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