Script: Store Tags in Custom Meta Data / Restore Tags from Custom Meta Data

These scripts can be used to store tags that are no longer used and to restore them if necessary.

  • The first script stores information about tags in Custom Meta Data and removes tags from selected records.
  • The second script reassigns tags.

Considerations

  • A tag can be renamed which means if we only store its name and later restore it then we could end up with a new tag.

  • A tag hierachy can be rearranged which means if we only store a tag’s location plus its name then we could end up with a new hierachy.

Using a tag’s UUID would solve both situations, but

  • A tag can be deleted which means if we only store its UUID then we couldn’t restore it if it was deleted in the meantime.

Approach

Therefore the script uses a mixed approach of storing,

  • the UUID, in order to be prepared for the first two situations and
  • the location plus the name, in order to be prepared for unintentionally and intentionally deleted tags.

This approach allows to

  • remove tags from records, i.e. clean up a tag’s visible content, e.g. reducing a tag’s child count
  • remove tags from records and (manually) delete the tags from the database, i.e. reduce the total number of a database’s tags

How it works

Two tags with the same name are not treated as equal. What counts is whether their UUID is equal.

Both scripts allow the choice what tags should be stored/restored, if property useChooseFromList is

  • false then all tags of selected records are immediately stored/restored without user interaction.
  • true then you can choose what to store/restore.

Script 1

This script stores the tag’s hierachy plus a delimiter plus its UUID. Each tag’s information is stored on a new line.

Script 2

This script changes behaviour depending on what happened to an original tag since it was stored.

Scenario - Tag not deleted

  • Restoring reassigns the original tag to the record

Scenario - Tag deleted

  • Restoring creates a new tag
  • Assigns the new tag to the record
  • Sets the new tag’s aliases to the original tag’s name plus its UUID
    Note: Do not delete the alias.

Usage Scenario

Usage Scenario 1

You’ve finished a project and don’t need or don’t want to see the tags used for this project anymore. Now

  • move all used tags under a new tag, e.g. “Finished Project XY”
  • select all its descendants,
  • run script 1,
  • finally delete the tag.

If you need to assign these tags again to the records in the future then running script 2 will rebuilt the tag hierachy nicely ordered under your “Finished Project XY” parent tag.

By the way, it’s possible to store and restore different “sets” or “projects” for a record. You can use set “A” today and set “B” tomorrow and set “C” next week - there’s no need to delete either of them. Just store what you currently don’t need and restore if it’s needed again.

Usage Scenario 2

You’re working on something that requires only a subset of tag “ABX”.

  • select all records that are currently not needed
  • run script 1

When you’re done do a toolbar search within your Custom Meta Data for “ABX”. This yields all records you’ve previously stored this tag for. Select the results and run script 2 to restore the tags.

Setup

In DEVONthink

  • Open Preferences > Data
  • Add new Custom Meta Data
    • Choose a meaningful name, e.g. “Ex Tags”
    • Set the type to either Single-line Text or Multi-line Text.
      Choose Single-line Text if you want to save space in the inspector.
      Choose Multi-line Text if you want to be able to see the stored data in the inspector.

In the script

  • Set property theMetaDataIdentifier to your Custom Meta Data identifier that’s shown in DEVONthink’s Preferences > Data.

  • Set property useChooseFromList to true of you’re sure that you always want to store/restore all tags of a record instead of having the choice what to store/restore.

Scripts

1 - Store Tags in Custom Meta Data

-- Store Tags in Custom Meta Data

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property theMetaDataIdentifier : "extags" -- replace this with your identifier
property useChooseFromList : true -- if false all tags are stored without user interaction

tell application id "DNtp"
	try
		set theRecords to selected records whose tag type ≠ ordinary tag
		if theRecords = {} then error "Please select some records"
		set theTab to (character id 9)
		
		repeat with thisRecord in theRecords
			set theTags_active to (parents of thisRecord whose tag type = ordinary tag)
			if theTags_active ≠ {} then
				set theTags_inactive_compound_string to get custom meta data for theMetaDataIdentifier from thisRecord
				if theTags_inactive_compound_string ≠ missing value and theTags_inactive_compound_string ≠ "" then
					set theTags_inactive_compound to my tid(theTags_inactive_compound_string, linefeed)
				else
					set theTags_inactive_compound to {}
				end if
				
				set theChooseFromListItems to {}
				set theTags_inactive_compound_new to {}
				repeat with thisTag in theTags_active
					set thisTag_UUID to uuid of thisTag
					set thisTag_Hierachy to my tid(items 3 thru -1 in my tid(location of thisTag, "/"), "/") & name of thisTag
					set end of theChooseFromListItems to thisTag_Hierachy & linefeed & thisTag_UUID
					set end of theTags_inactive_compound_new to thisTag_Hierachy & theTab & thisTag_UUID
				end repeat
				
				if useChooseFromList = true then
					set theChooseFromListItems to my sort(theChooseFromListItems)
					set theChoice to choose from list theChooseFromListItems with prompt "Store Tags:" default items (item 1 of theChooseFromListItems) with title (name of thisRecord as string) with multiple selections allowed
					if theChoice is false then return
					
					set theTags_inactive_compound_updated to theTags_inactive_compound
					set theTrashGroup to trash group of database of thisRecord
					repeat with thisItem in theChoice
						set thisTag_UUID to paragraph 2 of thisItem
						set thisTag to (get record with uuid thisTag_UUID)
						move record thisRecord from thisTag to theTrashGroup
						set end of theTags_inactive_compound_updated to paragraph 1 of thisItem & theTab & thisTag_UUID
					end repeat
					
				else
					set tags of thisRecord to (parents of thisRecord whose tag type ≠ ordinary tag)
					set theTags_inactive_compound_updated to theTags_inactive_compound & theTags_inactive_compound_new
				end if
				
				add custom meta data (my tid(my sort(theTags_inactive_compound_updated), linefeed)) for theMetaDataIdentifier to thisRecord
			end if
		end repeat
		
	on error error_message number error_number
		if the error_number is not -128 then
			activate
			display alert "DEVONthink" message error_message as warning
			return
		end if
	end try
end tell

on tid(theInput, theDelimiter)
	set d to AppleScript's text item delimiters
	set AppleScript's text item delimiters to theDelimiter
	if class of theInput = text then
		set theOutput to text items of theInput
	else if class of theInput = list then
		set theOutput to theInput as text
	end if
	set AppleScript's text item delimiters to d
	return theOutput
end tid

on sort(theList)
	try
		return ((current application's NSArray's arrayWithArray:theList)'s sortedArrayUsingSelector:"localizedStandardCompare:") as list
	on error error_message number error_number
		activate
		display alert "Error: Handler \"sort\"" message error_message as warning
		error number -128
	end try
end sort


2 - Restore Tags from Custom Meta Data

-- Restore Tags from Custom Meta Data

property theMetaDataIdentifier : "extags" -- replace this with your identifier
property useChooseFromList : true -- if false all tags are restored without user interaction

tell application id "DNtp"
	try
		set theRecords to selected records
		if theRecords = {} then error "Please select some records"
		set theTab to (character id 9)
		
		repeat with thisRecord in theRecords
			set theTags_inactive_compound_string to get custom meta data for theMetaDataIdentifier from thisRecord
			if theTags_inactive_compound_string ≠ missing value and theTags_inactive_compound_string ≠ "" then
				set theTags_inactive_compound to my tid(theTags_inactive_compound_string, linefeed)
				
				set theChooseFromListItems to {}
				set theTags_active_compound_new to {}
				repeat with thisTag_inactive_compound in theTags_inactive_compound
					set {thisTag_Hierachy, thisTag_UUID} to my tid(thisTag_inactive_compound, theTab)
					set end of theChooseFromListItems to thisTag_Hierachy & linefeed & thisTag_UUID
					set end of theTags_active_compound_new to thisTag_Hierachy & theTab & thisTag_UUID
				end repeat
				
				if useChooseFromList = true then
					
					set theChoice to choose from list theChooseFromListItems with prompt "Restore Tags:" default items (item 1 of theChooseFromListItems) with title (name of thisRecord as string) with multiple selections allowed
					if theChoice is false then return
					
					set theTags_active_compound_new to {}
					repeat with thisItem in theChoice
						set thisTag_UUID to paragraph 2 of thisItem
						set thisTag to (get record with uuid thisTag_UUID)
						if thisTag = missing value then set thisTag to my getTag(thisTag_UUID, thisTag_Hierachy, thisRecord)
						replicate record thisRecord to thisTag
						set end of theTags_active_compound_new to paragraph 1 of thisItem & theTab & thisTag_UUID
					end repeat
					
					set theTags_inactive_compound_updated to {}
					repeat with thisTag_inactive_compound in theTags_inactive_compound
						if thisTag_inactive_compound as string is not in theTags_active_compound_new then set end of theTags_inactive_compound_updated to thisTag_inactive_compound as string
					end repeat
					add custom meta data (my tid(theTags_inactive_compound_updated, linefeed)) for theMetaDataIdentifier to thisRecord
					
				else
					repeat with thisTag_active_compound_new in theTags_active_compound_new
						set {thisTag_Hierachy, thisTag_UUID} to my tid(thisTag_active_compound_new, theTab)
						set thisTag to (get record with uuid thisTag_UUID)
						if thisTag = missing value then set thisTag to my getTag(thisTag_UUID, thisTag_Hierachy, thisRecord)
						replicate record thisRecord to thisTag
					end repeat
					add custom meta data "" for theMetaDataIdentifier to thisRecord
				end if
			end if
		end repeat
		
	on error error_message number error_number
		if the error_number is not -128 then
			activate
			display alert "DEVONthink" message error_message as warning
			return
		end if
	end try
end tell

on tid(theInput, theDelimiter)
	set d to AppleScript's text item delimiters
	set AppleScript's text item delimiters to theDelimiter
	if class of theInput = text then
		set theOutput to text items of theInput
	else if class of theInput = list then
		set theOutput to theInput as text
	end if
	set AppleScript's text item delimiters to d
	return theOutput
end tid

on getTag(theTag_UUID, theTag_Hierachy, theRecord)
	tell application id "DNtp"
		try
			set theTag_Name to item -1 of my tid(theTag_Hierachy, "/")
			set theTag_Alias to theTag_Name & "_" & theTag_UUID
			set theTags to (parents of database of theRecord whose tag type = ordinary tag and aliases contains theTag_Alias)
			if theTags ≠ {} then
				set theTag to item 1 of theTags
			else
				set theTag to create location (name of tags group of database of theRecord) & "/" & theTag_Hierachy & "_" & theTag_UUID
				set name of theTag to theTag_Name
				set aliases of theTag to theTag_Alias
			end if
			return theTag
		on error error_message number error_number
			activate
			display alert "DEVONthink" message error_message as warning
			error number -128
		end try
	end tell
end getTag

3 Likes

:tada:

This is fantastic. I’m impressed at how you took into consideration the different scenarios, including tag renaming! The scheme seems quite thorough.

1 Like