#!/usr/bin/ruby
# Filename: insert_album
# Author: David Ljung Madison <DaveSource.com>
# See License:  http://MarginalHacks.com/License/
# Description:  Insert photos/videos into my photo albums according to date
# * Also attempts to put it in the correct location in the captions.txt
# * Handles .heic photos (iPhone) as well as ignoring 2s image movies (iPhone)
#
# TODO:  Consider dated subalbums??  Is this done??
# TODO:  Rotate videos (based on info in ffprobe) (put into xcode??)
require 'date'

def filename(path)
	path.sub(/.*\//,'')
end

PROGNAME = filename($0)


ALBUM = '/WWW/web/DavePics.com/Album'

INSERT_CAPTIONS = "BING__#{PROGNAME}"

ONLY = '^201.$'
IPHONE_PATHS = true

# For .heic images
HEIF_CONVERT = ['heif-convert','-q','80']

$DEBUG = false

def fatal(msg)
	$stderr.puts "[FATAL] #{msg}"
	exit -1
end

def is_movie(str)
	return str.match(/\.(mov|moov|avi|mp4)/i)
end

def is_image(str)
	return str.match(/\.(heic|jpg|jpeg|png|gif)/i)
end


DIRTIMES = Hash.new
def add_album(path)
	# Only albums that have a year and the final path component has a 'mon-day....'
	return unless path.match(/\/(\d{4})\/(.*\/)?(\d{1,2})-(\d{1,2})([^\/]+)$/)
	year,mon,day,extra = $1.to_i,$3.to_i,$4.to_i,$5
	return if extra.match(/^\d/)
	#puts "#{year}/#{mon}/#{day} - #{path}"
	begin
		dt = DateTime.new(year,mon,day)
	rescue => e
		return
	end
	#$stderr.puts "[WARN] Already had a directory for #{year}/#{mon}/#{day}\n  #{DIRTIMES[dt]}\n  #{path}\n" if DIRTIMES[dt]
	DIRTIMES[dt] = path
end

def collect_albums(dir,depth=0)
	Dir.foreach(dir) { |d|
		next unless depth>0 || d.match(/#{ONLY}/)
		next if d=='.'
		next if d=='..'
		next if d=='tn'

		path = dir+'/'+d

		next unless File.directory?(path)
		collect_albums(path,depth+1)
		add_album(path)
	}
end

DURATION = Hash.new
DTCACHE = Hash.new
def datetime(path)
	file = filename(path)
	qpath = Regexp.escape(path)
	return DTCACHE[path] if DTCACHE[path]

	trydate = file

#	# iphone paths
#	Nope.  This is the date of the import, probably - and the exif info is gone??
#	trydate = $2 if IPHONE_PATHS && path.match(/(^|\/)(\d{8}-\d{6})\/[^\/]+$/)

	# Get from exif info if possible
	# While we are there, grab duration for movies
	if path.match(/\.(jpg|jpeg|heic|mov|mp4)$/) && File.exist?(path)
# Hope that there are no 'drop tables' type path issues
		moddate = nil
		IO.popen("exiftool #{qpath}").each { |l|
			l.force_encoding('ISO-8859-1')
			# Some images have Modification Date bt not create Date??
			DURATION[path] = $1.to_f if l.match(/^Duration\s+:\s+(\d+(\.\d+)?)\s*s/)
			if l.match(/^.*Modification Date.*\s*:\s*(\d{4})[:-](\d{2})[:-](\d{2})\s+((\d{2})[:-](\d{2})[:-](\d{2}))?/) then
				year,mon,day, h,m,s = $1.to_i,$2.to_i,$3.to_i,($4?($5.to_i):0),($4?($6.to_i):0),($4?($7.to_i):0)
				moddate = DateTime.new(year,mon,day,h,m,s)
			end
			next unless l.match(/^(?:Create Date|Date Time Original|Creation Date)\s*:\s*(\d{4})[:-](\d{2})[:-](\d{2})\s+((\d{2})[:-](\d{2})[:-](\d{2}))?/)
			year,mon,day, h,m,s = $1.to_i,$2.to_i,$3.to_i,($4?($5.to_i):0),($4?($6.to_i):0),($4?($7.to_i):0)
			if year>0
#puts "dt$ #{path} is #{year} #{mon} #{day} #{h} #{m} #{s}"
				DTCACHE[path] ||= DateTime.new(year,mon,day,h,m,s)
				return DTCACHE[path] unless is_movie(path)	# So we can get duration
			end
		}
		# Did we at least find a moddate?
		DTCACHE[path] ||= moddate
		return DTCACHE[path] if DTCACHE[path]
	end

	# Get from exif info if possible
	if path.match(/\.(jpg|jpeg)$/) && File.exist?(path)
# Hope that there are no 'drop tables' type path issues
		IO.popen("jhead  #{qpath}").each { |l|
			l.force_encoding('ISO-8859-1')
			next unless l.match(/^Date\/Time\s*:\s*(\d{4})[:-](\d{2})[:-](\d{2})\s+((\d{2})[:-](\d{2})[:-](\d{2}))?/)
			year,mon,day, h,m,s = $1.to_i,$2.to_i,$3.to_i,($4?($5.to_i):0),($4?($6.to_i):0),($4?($7.to_i):0)
			return DTCACHE[path] = DateTime.new(year,mon,day,h,m,s)
		}
	end

	# Can we get it from the filename?
	if trydate.match(/^(img_)?(\d{4})-?(\d{2})-?(\d{2})([_-](\d{2})(\d{2})(\d{2}))?/i) then
		year,mon,day = $2.to_i,$3.to_i,$4.to_i
		h,m,s = $5?($6.to_i):0,$5?($7.to_i):0,$5?($8.to_i):0
		begin
			return DTCACHE[path] = DateTime.new(year,mon,day, h,m,s)
		rescue => e
		end
	end

	return nil
end

def datetimeCap(line,dir)
	return if line.match(/^#?\d{8}_\d{8}/)	# Facebook
	return unless line.match(/^#?([^\t]+)\t/)
	file = $1
	path = dir+'/'+file
	datetime(path)
end

def insert_album(path,datetime,dir)
	img = filename(path)
	puts "#{img} -> #{dir}"
	return if $DEBUG

	# For .heic, we need to convert
	heic = img.match(/(.+)\.heic$/i) ? img : nil
	if heic
		img = dir+'/'+$1+'.jpg'
		depth = path.sub(/\.heic$/i, '-depth.jpg')
		puts "Converting heic #{heic}"
		$DEBUG ? puts("% #{HEIFCONVERT} #{path} #{img}") : system(*HEIFCONVERT,path,img)
		File.delete depth
	end

	# Move mv the file
	File.rename(path,dir+'/'+img)

	captions = dir+'/captions.txt'
	return unless File.exist?(captions)

	# Update captions - try to insert it in the right spot if possible
	bak = captions+'.bak'
	File.rename captions, bak
	File.open(captions,'w') { |newcap|
		IO.foreach(bak) { |l|
			l.force_encoding('ISO-8859-1')
			img = nil if img && l.start_with?(img)
			img = nil if img && l.start_with?('#'+img)
			cdt = datetimeCap(l,dir)
			if cdt && img && cdt>datetime
				newcap.puts "#{img}\t#{INSERT_CAPTIONS}\t#{img}" 
				newcap.puts "##{heic}\tOriginal heic image" if heic
				img = nil
			end
			newcap.puts l
		}
		newcap.puts "#{img}\t#{INSERT_CAPTIONS}\t#{img}" if img
		newcap.puts "##{heic}\tOriginal heic image" if img && heic
	}
	File.delete bak
end

# Find closest album match for a given datetime
def find_album(datetime)
	DIRTIMES.keys.sort.reverse.each { |dt|
		return DIRTIMES[dt] if dt<=datetime
	}
	nil
end

CHANGED = Hash.new
def get_images(paths)
	paths = [paths] unless paths.class==Array
	paths.each { |path|
		if File.directory?(path)
			Dir.entries(path).each { |d|
				next if d=='.' or d=='..'
				get_images(path+'/'+d)
			}
		end

		next unless is_image(path) || is_movie(path)

		datetime = datetime(path)
		unless datetime
			$stderr.puts "#{filename(path)} -> No date found.  Skipping."
			next
		end

		# Check for iPhone snapshots - they are short and have a corresponding image
		if is_movie(path) && DURATION[path] && DURATION[path]<3.50
			base = path.sub(/\.[^\.\/]+$/,'')
			if File.exist?(base+'.jpg') || File.exist?(base+'.heic')
				puts "#{filename(path)} -> Skipping iPhone snapshot clip"
				next
			end
		end

		album = find_album(datetime)

		# Check if it already exists
		if File.exist? album+'/'+filename(path)
			puts "Duplicate: #{path}"
			File.delete? path
			next
		end

		insert_album(path,datetime,album)
		CHANGED[album] = 1
	}
end

# Main code
def main
	collect_albums(ALBUM)

	images = []
	ARGV.each { |arg|
		if arg=='-d' then $DEBUG=true; next; end
		images.push(arg)
	}

	get_images(images)

	puts "\nCHANGED ALBUMS:   # search captions.txt for \"#{INSERT_CAPTIONS}\""
	CHANGED.keys.sort.each { |changed|
		puts changed
	}
end
main()
