using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Media.Imaging; using System.Globalization; using System.Threading; namespace Image_Sorter { class Program { static readonly List DNGTypes = new List {"raw", "cr2", "dng"}; static readonly List ImagePrefixes = new List { "", "Screenshot_", "VID"}; static void Main(string[] args) { Console.WriteLine("Image Sorter {0} (C)2019 Brychan Dempsey"); Console.WriteLine("Moves images from the source folder and arranges them by date (from metadata) in the destination folder"); string SourceDir = ""; string DestinationDir = ""; bool copyFiles = false; if (args.Length > 0 && args[0].ToLower() == "\\s") { int sourceDirSpecified = Array.IndexOf(args, "\\d"); if (sourceDirSpecified != -1) { if (Directory.Exists(args[sourceDirSpecified + 1])) { SourceDir = args[sourceDirSpecified + 1]; } } int targetDirSpecified = Array.IndexOf(args, "\\t"); if (targetDirSpecified != -1) { if (Directory.Exists(args[targetDirSpecified + 1])) { DestinationDir = args[targetDirSpecified + 1]; } } else { DestinationDir = SourceDir; } if (args.Contains("\\c")) { copyFiles = true; } // Redirect all console writes Console.SetOut(new StringWriter()); } else if (args.Length > 0 && (args[0] == "\\?" || args[0].ToLower() == "\\h")) { Console.WriteLine("Image Sorter"); Console.WriteLine("Can be run in silent mode using the argument: \\s"); Console.WriteLine("Additional flags: \\d - Source Directory (Usage: \\s \\d "); Console.WriteLine("\t\t\\t - Target Directory (Usage: \\s \\t "); Console.WriteLine("\t\t\\c - Copy files if this flag is specified"); Console.WriteLine("Usage Example:"); Console.WriteLine("\\s \\d \"C:\\Camera Files\\Images\" \\t \"C:\\Camera Files\\Sorted\\\""); Console.WriteLine("\nIf the destination directory is not specified, it will default to the source directory"); Console.ReadLine(); Environment.Exit(0); } else { Console.WriteLine("Enter the source directory:"); SourceDir = Console.ReadLine(); Console.WriteLine("Enter the destionation directory:"); DestinationDir = Console.ReadLine(); Console.WriteLine("Enter \'y\' to perform a copy instead of move"); string tRead = Console.ReadLine(); if (tRead.Trim().ToLower().Equals("y")) { copyFiles = true; } } Console.WriteLine("Scanning the source directory..."); List SourceFiles = GetFiles(SourceDir); Console.WriteLine("Found {0} files", SourceFiles.Count()); int processedCount = 0; object countLock = new object(); Task updateFilesTask = new Task(() => { Parallel.ForEach(SourceFiles, (filepath) => { string fileDestination = ""; DateTime photoDate = new DateTime(); bool hasMetadata = true; using (FileStream fs = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)) { bool ruleMatch = false; Dictionary imgMetadata = GetMetaData(fs); if (imgMetadata.ContainsKey("date")) { photoDate = DateTime.Parse(imgMetadata["date"]); ruleMatch = true; } // If the file is a negative, it may have a conjugate image file else if (DNGTypes.Contains(Path.GetExtension(filepath).TrimStart('.'))) { List matches = SourceFiles.FindAll((x) => Path.GetFileNameWithoutExtension(x).Equals(Path.GetFileNameWithoutExtension(filepath)) && !Path.GetExtension(x).Equals(Path.GetExtension(filepath))); if (matches.Count == 1) { using (FileStream tfs = new FileStream(matches[0], FileMode.Open, FileAccess.Read, FileShare.Read)) { imgMetadata = GetMetaData(tfs); } if (imgMetadata.ContainsKey("date")) { photoDate = DateTime.Parse(imgMetadata["date"]); Console.WriteLine(" Conjugate file found - " + Path.GetFileName(filepath)); ruleMatch = true; } } } // Try parsing a date by the rules set in ImagePrefixes, using the first if found else if (ImagePrefixes.Exists((x) => TryParseDate(Path.GetFileNameWithoutExtension(filepath).TrimStart(x.ToCharArray()), out photoDate))) { Console.WriteLine(" Date successfully parsed from file: {0} - Parsed Date: {1} ", Path.GetFileName(filepath), photoDate.ToString("dd-MM-yyyy")); ruleMatch = true; } // Failsafe to avoid implausable dates (Greater than system year + 10, < 1990) if (ruleMatch && (photoDate.Year > DateTime.Now.Year + 10 || photoDate.Year < 1990)) { ruleMatch = false; } // Finally, resort to the least of the file creation time && file modified time if (!ruleMatch) { FileSystemInfo fileInfo = new FileInfo(filepath); photoDate = fileInfo.CreationTime < fileInfo.LastWriteTime ? fileInfo.CreationTime : fileInfo.LastWriteTime; } } fileDestination = "\\" + photoDate.Year; fileDestination += "\\" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(photoDate.Month); Directory.CreateDirectory(DestinationDir + fileDestination + "\\"); fileDestination += "\\" + filepath.Remove(0, filepath.LastIndexOf("\\")); if (copyFiles) { try { File.Copy(filepath, DestinationDir + fileDestination); filepath = DestinationDir + fileDestination; } catch { Console.WriteLine("Failed to copy {0}", Path.GetFileName(filepath)); } } else { try { File.Move(filepath, DestinationDir + fileDestination); filepath = DestinationDir + fileDestination; } catch { Console.WriteLine("Failed to move {0}", Path.GetFileName(filepath)); } } lock (countLock) { processedCount++; } }); }); int lastProcessed = 0; updateFilesTask.Start(); bool eval = false; while (!eval) { lock (countLock) { if (processedCount != lastProcessed) { Console.WriteLine("{0}/{1} processed.", processedCount, SourceFiles.Count); lastProcessed = processedCount; } if (processedCount == SourceFiles.Count) eval = true; } Thread.Sleep(10); } Console.ReadLine(); } static Dictionary GetMetaData(FileStream fileStream) { Dictionary Metadata = new Dictionary(); try { BitmapSource srcimg = BitmapFrame.Create(fileStream); BitmapMetadata md = (BitmapMetadata)srcimg.Metadata; string date = md.DateTaken; if (!string.IsNullOrEmpty(date)) { Metadata.Add("date", date); } } catch { Console.WriteLine("Couldn't get metadata - {0}", fileStream.Name); } return Metadata; } static List GetFiles(string SourceDirectory) { List foundFiles = Directory.GetFiles(SourceDirectory, "*", SearchOption.TopDirectoryOnly).ToList(); foreach (var subDirectory in Directory.GetDirectories(SourceDirectory)) { foundFiles.AddRange(GetFiles(subDirectory)); } return foundFiles; } static bool TryParseDate(string input, out DateTime dt) { // Attempt a base conversion first // As datetime's built-in converter does not seem to be able to handle more complex strings, // convert the to a usable format before attempting to convert a datetime //int pos = ImagePrefixes.FindIndex(0, (x) => input.StartsWith(x)); //input = input.Replace(ImagePrefixes[pos], ""); // Remove the prefix if (DateTime.TryParse(input, out dt)) return true; StringBuilder converted = new StringBuilder(); string modified = input; DateTime result = new DateTime(); // if we are in the format "ddddsddsdd" we can try to parse that date if (isMatch(input)) { int year = Convert.ToInt32(input.Substring(0, 4)); int month = Convert.ToInt32(input.Substring(5, 2)); int day = Convert.ToInt32(input.Substring(8, 2)); try { result = new DateTime(year, month, day); } catch { return false; } } // Else try the straight digit parsing, so ensure we have 8 contiguous digits to parse. else if (isMatch(input, "dddddddd")) { int year = Convert.ToInt32(input.Substring(0, 4)); int month = Convert.ToInt32(input.Substring(4, 2)); int day = Convert.ToInt32(input.Substring(6, 2)); try { result = new DateTime(year, month, day); } catch { return false; } } if (result.Year <= DateTime.Now.Year+10 && result.Year >= 1980) { // Falls within an acceptable date range, so try use it as a date dt = result; return true; } return false; } private static bool isNumeric(char c1) { if (c1 >= 48 && c1 < 58) { return true; } else return false; } private static bool isSymbol(char c1) { if ((c1 >= 32 && c1 < 48) || (c1 >= 58 && c1 < 65) || (c1 >= 91 && c1 < 97) || (c1 >= 123 && c1 < 127)) { return true; } else return false; } /// /// Checks if the provided string matches the format specified /// d = digit /// a = alphanumeric /// s = symbol /// * = any /// /// /// /// private static bool isMatch(string source, string format = "ddddsddsdd") { if (source.Length < format.Length) return false; bool state = true; for (int i = 0; i < format.Length; i++) { if (format[i] == 'd') { if (!isNumeric(source[i])) { return false; } } else if (format[i] == 's') { if (!isSymbol(source[i])) { return false; } } } return state; } } }