Script: Create Smart Groups for URL (sub)domains

This script creates Smart Groups for group childrens’ URL (sub)domains.

Usage

Select some groups (in the item list) or a database’s root (in the navigation sidebar).

Note

If property useSubdomain is set to true and there’s e.g.

  • a record with domain substack.com that got no subdomain
  • a record with subdomain edwardsnowden.substack.com

then two Smart Groups are created:

  • url:substack.com url:!~edwardsnowden.substack.com (i.e. excluding the subdomain)
  • url:edwardsnowden.substack.com

This means if you add new records the first Smart Group’s query may match records that you don’t want to be matched (because it doesn’t know about the new subdomain that you want it to ignore).

Delete the Smart Groups and run the script again to make sure everything’s up to date.

Properties

  • excludeSubgroups : search in subgroups. this is ignored if a database’s root is selected

  • useSubdomain : use subdomain for searching, e.g. discourse.devontechnologies.com instead of devontechnologies.com

  • theIgnoredSubdomains : list of subdomains that should be ignored. order is important

  • useOnlySubdomainInName : omit domain, e.g. discourse.devontechnologies.com instead of devontechnologies.com | discourse.devontechnologies.com

  • theNamePrefix : add custom prefix, e.g. Domain:

  • useGroupNameInName : add suffix in: Group XY

  • excludeFromSearch : exclude Smart Groups from search

-- Create Smart Groups for URL (sub)domains

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

property excludeSubgroups : false -- this is ignored if a database's root is selected
property useSubdomain : true -- use subdomain for searching (e.g. "discourse.devontechnologies.com" instead of domain "devontechnologies.com")
property theIgnoredSubdomains : {"www", "de.m", "de", "en.m", "en", "m", "mobile"} -- subdomains that should be ignored. order is important
property useOnlySubdomainInName : false -- omit domain (e.g. ""discourse.devontechnologies.com" instead of "devontechnologies.com | discourse.devontechnologies.com")
property theNamePrefix : "" -- add custom prefix (e.g. "Domain: ")
property useGroupNameInName : false -- add suffix "in: " plus selected group's name 
property excludeFromSearch : false -- exclude Smart Groups from search

tell application id "DNtp"
	try
		set theRecords to selected records whose type = group
		set theErrorMessage to "Please select the database's root in the sidebar." & linefeed & "Or select some groups in the item list."
		
		if theRecords ≠ {} then
			show progress indicator "Creating Smart Groups ... " steps ((count of theRecords) * 3) with cancel button
			
			repeat with thisRecord in theRecords
				step progress indicator "... " & (name of thisRecord)
				set theTempSmartGroup to create record with {name:"temp", type:smart group, search predicates:"url!=", search group:thisRecord, exclude subgroups:excludeSubgroups, exclude from search:true} in thisRecord
				set theURLs to URL of children of theTempSmartGroup
				delete record theTempSmartGroup
				set theSmartGroupProperties to my createSmartGroupProperties(theURLs)
				step progress indicator "... " & (name of thisRecord)
				my createSmartGroup(theSmartGroupProperties, thisRecord, excludeSubgroups)
				step progress indicator "... " & (name of thisRecord)
			end repeat
			
		else
			
			show progress indicator "Creating Smart Group ... " steps 3 with cancel button
			if not (exists think window 1) then error theErrorMessage
			set theRecord to root of think window 1
			if theRecord ≠ root of current database then error theErrorMessage
			set theURLs to URL of children of theRecord whose URL ≠ ""
			step progress indicator "Getting properties ... "
			set theSmartGroupProperties to my createSmartGroupProperties(theURLs)
			step progress indicator "Creating Smart Group ... "
			my createSmartGroup(theSmartGroupProperties, theRecord, true)
			step progress indicator "Creating Smart Group ... "
		end if
		
		hide progress indicator
	on error error_message number error_number
		hide progress indicator
		if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
		return
	end try
end tell

on createSmartGroupProperties(theURLs)
	try
		set theDomains to {}
		set theDomainsAndSubdomains to {}
		
		repeat with thisURL in theURLs
			set thisURL to thisURL as string
			if thisURL does not contain "@" then
				set {thisDomain, thisDomainAndSubdomain} to my getDomainAndDomainWithSubdomains(thisURL)
				if {thisDomain, thisDomainAndSubdomain} ≠ {missing value, missing value} then
					
					if not useSubdomain then
						if thisDomain is not in theDomains then set end of theDomains to thisDomain
					else
						if thisDomainAndSubdomain is not in theDomainsAndSubdomains then
							set thisDomainsAndSubdomains_modified to thisDomainAndSubdomain
							
							repeat with thisIgnoredSubdomain in theIgnoredSubdomains
								set thisIgnoredSubdomain to thisIgnoredSubdomain as string
								if thisDomainsAndSubdomains_modified starts with (thisIgnoredSubdomain & ".") then
									set thisDomainsAndSubdomains_modified to (characters ((length of thisIgnoredSubdomain) + 2) thru -1 in thisDomainsAndSubdomains_modified) as string
								end if
							end repeat
							
							if thisDomainsAndSubdomains_modified is not in theDomainsAndSubdomains then
								set end of theDomains to thisDomain
								set end of theDomainsAndSubdomains to thisDomainsAndSubdomains_modified
							end if
						end if
					end if
					
				end if
			end if
		end repeat
		
		set theDomainsArray to current application's NSMutableArray's arrayWithArray:theDomains
		set theDomainAndSubdomainsArray to current application's NSMutableArray's arrayWithArray:theDomainsAndSubdomains
		set theArray to current application's NSMutableArray's new()
		
		if useSubdomain then
			try
				repeat with i from 0 to ((theDomainAndSubdomainsArray's |count|()) - 1)
					set thisDomainAndSubdomain to (theDomainAndSubdomainsArray's objectAtIndex:i)
					set thisDomain to (theDomainsArray's objectAtIndex:i)
					
					set thisPredicate_string to ("self ENDSWITH " & quoted form of (thisDomain as string)) & " AND " & ("self != " & quoted form of (thisDomainAndSubdomain as string))
					set thisDomain_Exclusions to (theDomainAndSubdomainsArray's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:(thisPredicate_string)))
					
					if (thisDomainAndSubdomain's isEqualTo:thisDomain) then
						set thisSmartGroup_Predicate_URLExclusions_list to thisDomain_Exclusions as list
						set thisSmartGroup_Name to thisDomainAndSubdomain
					else
						set thisSmartGroup_Predicate_URLExclusions_list to {}
						if useOnlySubdomainInName then
							set thisSmartGroup_Name to theNamePrefix & (thisDomainAndSubdomain as string)
						else
							set thisSmartGroup_Name to theNamePrefix & (thisDomain as string) & " | " & (thisDomainAndSubdomain as string)
						end if
					end if
					
					set thisSmartGroup_Predicate_URL to thisDomainAndSubdomain
					set thisSmartGroup_Predicate to "url:" & (thisSmartGroup_Predicate_URL as string)
					if thisSmartGroup_Predicate_URLExclusions_list ≠ {} then
						if (count of thisSmartGroup_Predicate_URLExclusions_list) = 1 then
							set thisSmartGroup_Predicate to thisSmartGroup_Predicate & space & "url:!~" & item 1 of thisSmartGroup_Predicate_URLExclusions_list
						else
							set thisSmartGroup_Predicate to thisSmartGroup_Predicate & space & "{url:!~" & my tid(thisSmartGroup_Predicate_URLExclusions_list, " url:!~") & "}"
						end if
					end if
					
					(theArray's addObject:{|SmartGroup_Name|:thisSmartGroup_Name, |SmartGroup_Predicate|:thisSmartGroup_Predicate, |SmartGroup_Predicate_URL|:thisSmartGroup_Predicate_URL, |SmartGroup_Predicate_URLExclusions|:thisSmartGroup_Predicate_URLExclusions_list})
				end repeat
			end try
			
		else
			
			repeat with i from 0 to ((theDomainsArray's |count|()) - 1)
				set thisDomain to (theDomainsArray's objectAtIndex:i)
				set thisSmartGroup_Name to theNamePrefix & (thisDomain as string)
				set thisSmartGroup_Predicate_URL to thisDomain
				set thisSmartGroup_Predicate to "url:" & (thisSmartGroup_Predicate_URL as string)
				(theArray's addObject:{|SmartGroup_Name|:thisSmartGroup_Name, |SmartGroup_Predicate|:thisSmartGroup_Predicate, |SmartGroup_Predicate_URL|:thisSmartGroup_Predicate_URL, |SmartGroup_Predicate_URLExclusions|:{}})
			end repeat
			
		end if
		
		return theArray as list
		
	on error error_message number error_number
		activate
		if the error_number is not -128 then display alert "Error: Handler \"createSmartGroupProperties\"" message error_message as warning
		error number -128
	end try
end createSmartGroupProperties

on getDomainAndDomainWithSubdomains(theURL_string) -- based on https://forum.latenightsw.com/t/how-to-extract-the-domain-from-an-url-with-applescriptobjc/3285/5
	try
		set theURL_DomainWithSubdomains to (DNS form of host of (theURL_string as URL))
		set d to AppleScript's text item delimiters
		set AppleScript's text item delimiters to "."
		set theURL_DomainWithSubdomains_TextItems to text items of theURL_DomainWithSubdomains
		set theURL_Domain to theURL_DomainWithSubdomains_TextItems
		
		repeat
			tell ("mailto:" & (rest of theURL_Domain)) as URL to tell host & {dotted decimal form:false}
				if (dotted decimal form ≠ false) then
					set theURL_Domain to rest of theURL_Domain
				else
					exit repeat
				end if
			end tell
		end repeat
		
		set theURL_Domain to theURL_Domain as string
		set AppleScript's text item delimiters to d
		return {theURL_Domain, theURL_DomainWithSubdomains}
	on error error_message number error_number
		return {missing value, missing value}
	end try
end getDomainAndDomainWithSubdomains

on createSmartGroup(theSmartGroupProperties, theGroup, excludeSubgroups)
	try
		tell application id "DNtp"
			repeat with theseSmartGroupProperties in theSmartGroupProperties
				set thisSmartGroup_Predicate to |SmartGroup_Predicate| of theseSmartGroupProperties
				set thisSmartGroup_Name to |SmartGroup_Name| of theseSmartGroupProperties
				
				if not (my existsSmartGroup(thisSmartGroup_Predicate, theGroup)) then
					if useGroupNameInName then
						set thisSmartGroup_Name to thisSmartGroup_Name & space & "in:" & space & name of theGroup
					end if
					set theSmartGroup to create record with {name:thisSmartGroup_Name, type:smart group, search predicates:thisSmartGroup_Predicate, search group:theGroup, exclude subgroups:excludeSubgroups, exclude from search:excludeFromSearch} in theGroup
				end if
				
			end repeat
		end tell
	on error error_message number error_number
		activate
		if the error_number is not -128 then display alert "Error: Handler \"createSmartGroup\"" message error_message as warning
		error number -128
	end try
end createSmartGroup

on existsSmartGroup(theSmartGroup_Predicate, theGroup)
	tell application id "DNtp"
		try
			set theGroup_Database to database of theGroup
			
			if theGroup = root of theGroup_Database then
				set theGroup_LocationAndName to "/"
			else
				set theGroup_Name to name of theGroup
				if theGroup_Name does not contain "/" then
					set theGroup_LocationAndName to location of theGroup & theGroup_Name & "/"
				else
					set theGroup_LocationAndName to location of theGroup & my escapeSlash(theGroup_Name) & "/"
				end if
			end if
			
			set theResults to (smart groups of theGroup_Database whose search predicates = theSmartGroup_Predicate and search group = theGroup and location = theGroup_LocationAndName)
			
			if theResults = {} then
				return false
			else
				return true
			end if
		on error error_message number error_number
			activate
			if the error_number is not -128 then display alert "Error: Handler \"existsSmartGroup\"" message error_message as warning
			error number -128
		end try
	end tell
end existsSmartGroup

on escapeSlash(theText)
	set d to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "/"
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to "\\/"
	set theText_escaped to theTextItems as string
	set AppleScript's text item delimiters to d
	return theText_escaped
end escapeSlash

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