Move Local Media to Media Records.fh_lua

--[[
@Title:			Move Local Media to Media Records
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			1.1
@Keywords:		
@LastUpdated:	18 Dec 2020
@Licence:			This plugin is copyright (c) 2020 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description:	Move all Local Media Objects (LMO) to Media Records (including UDF but not Sort Date LMO).
@V1.1:				Check if target file exists, cater for all possible Media tags for GEDCOM 5.5.1 FH V7 Lua 3.5 IUP 3.28;
@V1.0:				First published version.
@V0.1-V0.2:		Prototypes.
]]

if fhGetAppVersion() > 5 then fhSetStringEncoding("UTF-8") end	-- Cater for FH V6 Unicode

--[[
@Module:			+fh+progbar_v3
@Author:			Mike Tate
@Version:			3.0
@LastUpdated:	27 Aug 2020
@Description:	Progress Bar library module.
@V3.0:				Function Prototype Closure version.
@V1.0:				Initial version.
]]

local function progbar_v3()

	local fh = {}													-- Local environment table

	require "iuplua"												-- To access GUI window builder

	iup.SetGlobal("CUSTOMQUITMESSAGE","YES")					-- Needed for IUP 3.28

	local tblBars = {}												-- Table for optional external attributes
	local strBack = "255 255 255"								-- Background colour default is white
	local strBody = "0 0 0"										-- Body text colour default is black
	local strFont = nil												-- Font dialogue default is current font
	local strStop = "255 0 0"										-- Stop button colour default is red
	local intPosX = iup.CENTER									-- Show window default position is central
	local intPosY = iup.CENTER
	local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
	local lblText, barGauge, lblDelta, btnStop, dlgGauge

	local function doFocus()										-- Bring the Progress Bar window into Focus
		dlgGauge.BringFront="YES"									-- If used too often, inhibits other windows scroll bars, etc
	end -- local function doFocus

	local function doUpdate()										-- Update the Progress Gauge and the Delta % with clock
		barGauge.Value = intVal
		lblDelta.Title = string.format("%4d %%      %s ",math.floor(intPercent),strClock)
	end -- local function doUpdate

	local function doReset()										-- Reset all dialogue variables and Update display
		intVal		= 0													-- Current value of Progress Bar
		intPercent= 0.01											-- Percentage of progress
		intStart	= os.time()										-- Start time of progress
		intDelta	= 0													-- Delta time of progress
		intScale	= math.ceil( intMax / 1000 )					-- Scale of percentage per second of progress (initial guess is corrected in Step function)
		strClock	= "00 : 00 : 00"								-- Clock delta time display
		isBarStop	= false											-- Stop button pressed signal
		doUpdate()
		doFocus()
	end -- local function doReset

	function fh.Start(strTitle,intMaximum)						-- Create & start Progress Bar window
		if not dlgGauge then
			strTitle	= strTitle or ""							-- Dialogue and button title
			intMax		= intMaximum or 100							-- Maximun range of Progress Bar, default is 100
			local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30"			-- Adjust Stop button size to Title
			lblText	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
			barGauge	= iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
			lblDelta	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
			btnStop	= iup.button	{ Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; }	-- Signal Stop button pressed	return iup.CLOSE -- Often caused main GUI to close !!!
			dlgGauge	= iup.dialog	{ Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES";	-- Remove Windows minimize/maximize menu
								iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
									lblText;
									barGauge;
									lblDelta;
									btnStop;
								};
								move_cb	= function(self,x,y) tblBars.X = x tblBars.Y = y end;
								close_cb	= btnStop.action;		-- Windows Close button = Stop button
							}
			if type(tblBars.GUI) == "table"
			and type(tblBars.GUI.ShowDialogue) == "function" then
				dlgGauge.move_cb = nil								-- Use GUI library to show & move window
				tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
			else
				dlgGauge:showxy(intPosX,intPosY)				-- Show the Progress Bar window
			end
			doReset()													-- Reset the Progress Bar display
		end
	end -- function Start

	function fh.Message(strText)									-- Show the Progress Bar message
		if dlgGauge then lblText.Title = strText end
	end -- function Message

	function fh.Step(intStep)										-- Step the Progress Bar forward
		if dlgGauge then
			intVal = intVal + ( intStep or 1 )					-- Default step is 1
			local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
			if intPercent ~= intNew then							-- Update progress once per percent or per second, whichever is smaller
				intPercent = math.max( 0.1, intNew )			-- Ensure percentage is greater than zero
				if intVal > intMax then intVal = intMax intPercent = 100 end		-- Ensure values do not exceed maximum
				intNew = os.difftime(os.time(),intStart)
				if intDelta < intNew then							-- Update clock of elapsed time
					intDelta = intNew
					intScale = math.ceil( intDelta / intPercent )	-- Scale of seconds per percentage step
					local intHour = math.floor( intDelta / 3600 )
					local intMins = math.floor( intDelta / 60 - intHour * 60 )
					local intSecs = intDelta - intMins * 60 - intHour * 3600
					strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
				end
				doUpdate()											-- Update the Progress Bar display
			end
			iup.LoopStep()
		end
	end -- function Step

	function fh.Focus()												-- Bring the Progress Bar window to front
		if dlgGauge then doFocus() end
	end -- function Focus

	function fh.Reset()												-- Reset the Progress Bar display
		if dlgGauge then doReset() end
	end -- function Reset

	function fh.Stop()												-- Check if Stop button pressed
		iup.LoopStep()
		return isBarStop
	end -- function Stop

	function fh.Close()												-- Close the Progress Bar window
		isBarStop = false
		if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
	end -- function Close

	function fh.Setup(tblSetup)									-- Setup optional table of external attributes
		if tblSetup then
			tblBars = tblSetup
			strBack = tblBars.Back or strBack					-- Background colour
			strBody = tblBars.Body or strBody					-- Body text colour
			strFont = tblBars.Font or strFont					-- Font dialogue
			strStop = tblBars.Stop or strStop					-- Stop button colour
			intPosX = tblBars.X or intPosX						-- Window position
			intPosY = tblBars.Y or intPosY
		end
	end -- function Setup

	return fh

end -- local function progbar_v3

local progbar = progbar_v3()										-- To access FH progress bars module

require "lfs"																	-- To access LUA filing system

function FlgFileExists(strFileName) 										-- Check if file exists
	return lfs.attributes(strFileName,"mode") == "file"
end -- function FlgFileExists

function CopyFile(strSource,strTarget)										-- Copy media file and modification date-time
	local fileSource = assert(io.open(strSource,"rb"))
	local fileTarget = assert(io.open(strTarget,"wb"))
	local anyContent = ""
	repeat																			-- Copy entire file contents
		fileTarget:write(anyContent)
		anyContent = fileSource:read(10000)
	until anyContent == nil
	assert(fileSource:close())
	assert(fileTarget:close())
	lfs.touch(strTarget,os.time(),lfs.attributes(strSource,"modification"))
end -- function CopyFile

function NoteChildBranch(ptrSource,strTarget)							-- Note child branch from source
	return NoteChildren(ptrSource,strTarget..fhGetDisplayText(ptrSource))
end -- function NoteChildBranch

function NoteChildren(ptrSource,strTarget)								-- Note child fields from source
	local ptrFrom = ptrSource:Clone()
	ptrFrom:MoveToFirstChildItem(ptrFrom)
	while ptrFrom:IsNotNull() do
		strTarget = NoteChildBranch(ptrFrom,strTarget)
		ptrFrom:MoveNext()
	end
	return strTarget
end -- function NoteChildren

local ptrFile = fhNewItemPtr()												-- V1.1

function CopyChildBranch(ptrSource,ptrTarget,arrTags)					-- Copy child branch from source to target
	local strTag = fhGetTag(ptrSource)
	strTag = arrTags[strTag] or strTag										-- Cater for FILE and NOTE tag
	if strTag == "TITL" and fhGetAppVersion() > 6 then
		ptrTarget = ptrFile:Clone()											-- GEDCOM 5.5.1 TITL is per FILE in OBJE	-- V1.1
	end
	local ptrNew = fhCreateItem(strTag,ptrTarget,true)
	if ptrNew:IsNotNull() then
		fhSetValue_Copy(ptrNew,ptrSource)
		if strTag == "FILE" then ptrFile = ptrNew:Clone() end			-- GEDCOM 5.5.1 TITL is per FILE in OBJE	-- V1.1
	else
		local ptrNew = fhCreateItem("REFN",ptrTarget,true)				-- Cater for UDF and multiple NOTE tags, etc
		fhSetValueAsText(ptrNew,fhGetDisplayText(ptrSource))
	end
	CopyChildren(ptrSource,ptrNew)
end -- function CopyChildBranch

function CopyChildren(ptrSource,ptrTarget,arrTags)						-- Copy child fields from source to target
	local ptrFrom = ptrSource:Clone()
	ptrFrom:MoveToFirstChildItem(ptrFrom)
	while ptrFrom:IsNotNull() do
		CopyChildBranch(ptrFrom,ptrTarget,arrTags or {})
		ptrFrom:MoveNext()
	end
end -- function CopyChildren

function IsSortDate(ptrItem)													-- Does LMO conform to Sort Date format
	return fhGetItemText(ptrItem,"~.TITL") == "Sort Date" and fhGetItemText(ptrItem,"~.FILE") == ""
end -- function IsSortDate

local dicTags = {}																-- LMO to Record tag mapping for FH V7
local strFILE = "~.FILE"
if fhGetAppVersion() < 7 then 
		dicTags = { FILE="_FILE"; NOTE2="_NOTE"; }						-- LMO to Record tag mapping for FH V6
		strFILE = "~._FILE"
end

local arrRecs = {																-- Record type to Name and Keywords mapping
	{ Type = "INDI"	; Name = "Individual"	; Keys = "Picture"	; };
	{ Type = "FAM"	; Name = "Family"		; Keys = "Picture"	; };
	{ Type = "SOUR"	; Name = "Source"		; Keys = "Document"	; };
	{ Type = "SUBM"	; Name = "Submitter"	; Keys = "Picture"	; };
}

local strData = fhGetContextInfo("CI_PROJECT_DATA_FOLDER").."\\"	-- Project Data folder

function MoveLMO()																-- Move each LMO to a Media Record
	local dicObje = { }
	local arrItem = { }
	local intRecs = 0
	local ptrItem = fhNewItemPtr()
	for _, dicRec in ipairs (arrRecs) do									-- Count all records that may contain LMO
		ptrItem:MoveToFirstRecord(dicRec.Type)
		while ptrItem:IsNotNull() do
			intRecs = intRecs + 1
			ptrItem:MoveNext()
		end
	end
	progbar.Setup()																-- Progress Bar prototype
	if intRecs > 100 then
		progbar.Start("Moving Local Media to Records",intRecs)			-- Start the Progress Bar with number of required Records
	end
	local intFile = 0
	ptrItem:MoveToFirstRecord("OBJE")
	while ptrItem:IsNotNull() and math.abs(intFile) < 1000 do			-- Search Media records to see if Media were imported or not
		local strFile = fhGetItemText(ptrItem,strFILE)					-- V1.1
		if strFile:lower():match("^media\\") then
			if FlgFileExists(strData..strFile) then
				intFile = intFile + 1											-- Increment count for relative imported Media links
			end
		else
			if FlgFileExists(strFile) then
				intFile = intFile - 1											-- Decrement count for absolute external Media links
			end
		end
		ptrItem:MoveNext()
	end
	for _, dicRec in ipairs (arrRecs) do									-- Search all records that may contain LMO
		ptrItem:MoveToFirstRecord(dicRec.Type)
		while ptrItem:IsNotNull() do
			local strTag = fhGetTag(ptrItem)
			if ( strTag == "OBJE2" and not IsSortDate(ptrItem) )		-- Found LMO that is not a Sort Date
			or ( strTag == "OBJE" and fhIsUDF(ptrItem) ) then			-- Found LMO that is a UDF from Legacy
				local strObje = NoteChildren(ptrItem,"")		--? ,dicTags)
				if #strObje > 9 then
					local ptrObje = dicObje[strObje]						-- Check if LMO is a duplicate
					if not ptrObje then
						ptrObje = fhCreateItem("OBJE")						-- If not, then create new Media record
						if ptrObje:IsNotNull() then
							dicObje[strObje] = ptrObje:Clone()
							CopyChildren(ptrItem,ptrObje,dicTags)			-- Copy child fields and add Keyword
							fhSetValueAsText(fhCreateItem("_KEYS",ptrObje),dicRec.Keys)
							if intFile > 1 then 								-- Relative internal links prevail so copy Media file into Media folder
								local ptr_File  = fhGetItemPtr(ptrObje,strFILE)	-- V1.1
								local strSource = fhGetValueAsText(ptr_File)
								local strTarget = strSource:gsub(".-([^\\]+)$","Media\\%1")
								fhSetValueAsText(ptr_File,strTarget)		-- Set relative file link
								strTarget = strData..strTarget
								if FlgFileExists(strSource) and not FlgFileExists(strTarget) then	-- V1.1
									CopyFile(strSource,strTarget)
								end
							end
						end
					end
					if ptrObje:IsNotNull() then								-- Save pointers to LMO and Record
						table.insert(arrItem,{ Loc=ptrItem:Clone(); Rec=ptrObje:Clone(); } )
					end
				end
			end
			local intRid  = fhGetRecordId(ptrItem)							-- Obtain the Record Id
			if intRid > 0 then
				progbar.Message(dicRec.Name.." Record Id "..intRid) 	-- Update the Progress Bar per Record
				progbar.Step(1)
				isStop = progbar.Stop()
				if isStop then break end										-- Break out of inner loop
			end
			ptrItem:MoveNextSpecial()
		end
		if isStop then break end												-- Break out of outer loop
	end
	for _, dicItem in ipairs ( arrItem ) do
		local ptrItem = dicItem.Loc											-- Delete this Local Media Object item
		local ptrObje = dicItem.Rec											-- Link this Media Record to parent of LMO
		local ptrRoot = fhNewItemPtr()
		ptrRoot:MoveToParentItem(ptrItem) 
		local ptrLink = fhCreateItem("OBJE",ptrRoot)
		if fhIsUDF(ptrLink) or ptrLink:IsNull() then						-- Cater for LMO UDF from Legacy
			ptrRoot:MoveToParentItem(ptrRoot) 
			ptrLink = fhCreateItem("OBJE",ptrRoot)
		end
		ptrRoot:MoveToParentItem(ptrItem)
		local isOK = fhSetValueAsLink(ptrLink,ptrObje)
		local isOK = fhDeleteItem(ptrItem)									-- Only children of UDF get deleted (bug)
		ptrItem = fhGetItemPtr(ptrRoot,"~.OBJE")
		if fhIsUDF(ptrItem) then
			isOK = fhDeleteItem(ptrItem)										-- Delete the UDF itself
		end
		progbar.Step(1)
	end
	progbar.Close()																-- Close the Progress Bar
	fhMessageBox( "\n " .. #arrItem .. " LMO converted into Media records. \n\n Use 'Edit > Undo Plugin Updates' to undo conversions. \n" )
end -- function MoveLMO

local strMsg = [[
This converts each Local Media Object to a Media Record
  ( except those that conform to the "Sort Date" format. )

Any UDF or other exceptions are saved in Custom Id fields.

Continue?
]]

if fhMessageBox(strMsg,"MB_OKCANCEL","MB_ICONQUESTION") == "OK" then
	MoveLMO()
end

Source:Move-Local-Media-to-Media-Records-1.fh_lua