code:
import time, threading

from lib.Networking.Jobs import NetworkJobWatcherPage

from lib.Actions.Import import GetRepeatingJobInitialDelay, NetworkJobPresentationContext, \
	PageImporterShouldStopWorking, \
	UpdateFileSeedCacheWithFileSeeds, WakeRepeatingJob
from lib.Actions.Import.Options import CheckerOptions, FileImportOptions, TagImportOptions
from lib.Actions.Import.FileSeeds import FileSeedCache, GenerateFileSeedCachesStatus
from lib.Actions.Import.GallerySeeds import GallerySeed, GallerySeedLog

from lib.Hydrus.Data import GenerateKey, GetNow, GetNowPrecise, PrintException, \
	ShowException, TimeHasPassed, TimestampToPrettyTimeDelta
from lib.Hydrus.Exceptions import NetworkException
from lib.Hydrus.Serialisable import CreateFromSerialisableTuple, SerialisableBase, SerialisableList
from lib.Hydrus.Tags.Local import ServiceKeysToTags

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants.Local as CC

from lib.Actions.Import import CHECKER_STATUS_404, CHECKER_STATUS_DEAD, \
	CHECKER_STATUS_OK, DID_SUBSTANTIAL_FILE_WORK_MINIMUM_SLEEP_TIME, \
	REPEATING_JOB_TYPICAL_PERIOD

from lib.Hydrus.Serialisable import HG.SerialisableType["WATCHER_IMPORT"], \
	HG.SerialisableType["MULTIPLE_WATCHER_IMPORT"], SERIALISABLE_TYPES_TO_OBJECT_TYPES

### CLASSES ###

class MultipleWatcherImport( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["MULTIPLE_WATCHER_IMPORT"]
	SERIALISABLE_NAME = 'Multiple Watcher'
	SERIALISABLE_VERSION = 2

	ADDED_TIMESTAMP_DURATION = 15

	def __init__( self, url = None ):
		SerialisableBase.__init__( self )

		self._lock = threading.Lock()

		self._page_key = 'initialising page key'

		self._watchers = SerialisableList()

		self._highlighted_watcher_url = None

		self._checker_options = HG.client_controller.new_options.GetDefaultWatcherCheckerOptions()
		self._file_import_options = HG.client_controller.new_options.GetDefaultFileImportOptions( 'loud' )
		self._tag_import_options = TagImportOptions( is_default = True )

		self._watcher_keys_to_watchers = {}

		self._watcher_keys_to_added_timestamps = {}
		self._watcher_keys_to_already_in_timestamps = {}

		self._watchers_repeating_job = None

		self._status_dirty = True
		self._status_cache = None
		self._status_cache_generation_time = 0

		#

		if url is not None:
			watcher = WatcherImport()

			watcher.SetURL( url )

			self._AddWatcher( watcher )

		self._last_time_watchers_changed = GetNowPrecise()

		self._last_pubbed_value_range = ( 0, 0 )
		self._next_pub_value_check_time = 0

	def _AddWatcher( self, watcher ):
		watcher.PublishToPage( False )
		watcher.Repage( self._page_key )

		self._watchers.append( watcher )

		self._last_time_watchers_changed = GetNowPrecise()

		watcher_key = watcher.GetWatcherKey()

		self._watcher_keys_to_watchers[ watcher_key ] = watcher
		self._watcher_keys_to_added_timestamps[ watcher_key ] = GetNow()

	def _CleanAddedTimestamps( self ):
		keys = list( self._watcher_keys_to_added_timestamps.keys() )

		for key in keys:
			if TimeHasPassed( self._watcher_keys_to_added_timestamps[ key ] + self.ADDED_TIMESTAMP_DURATION ):
				del self._watcher_keys_to_added_timestamps[ key ]

		keys = list( self._watcher_keys_to_already_in_timestamps.keys() )

		for key in keys:
			if TimeHasPassed( self._watcher_keys_to_already_in_timestamps[ key ] + self.ADDED_TIMESTAMP_DURATION ):
				del self._watcher_keys_to_already_in_timestamps[ key ]

	def _GetSerialisableInfo( self ):
		serialisable_watchers = self._watchers.GetSerialisableTuple()

		serialisable_checker_options = self._checker_options.GetSerialisableTuple()
		serialisable_file_import_options = self._file_import_options.GetSerialisableTuple()
		serialisable_tag_import_options = self._tag_import_options.GetSerialisableTuple()

		return ( serialisable_watchers, self._highlighted_watcher_url, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( serialisable_watchers, self._highlighted_watcher_url, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options ) = serialisable_info

		self._watchers = CreateFromSerialisableTuple( serialisable_watchers )

		self._watcher_keys_to_watchers = { watcher.GetWatcherKey() : watcher for watcher in self._watchers }

		self._checker_options = CreateFromSerialisableTuple( serialisable_checker_options )
		self._file_import_options = CreateFromSerialisableTuple( serialisable_file_import_options )
		self._tag_import_options = CreateFromSerialisableTuple( serialisable_tag_import_options )

	def _RegenerateStatus( self ):
		file_seed_caches = [ watcher.GetFileSeedCache() for watcher in self._watchers ]

		self._status_cache = GenerateFileSeedCachesStatus( file_seed_caches )

		self._status_dirty = False
		self._status_cache_generation_time = GetNow()

	def _RemoveWatcher( self, watcher_key ):
		if watcher_key not in self._watcher_keys_to_watchers:
			return

		watcher = self._watcher_keys_to_watchers[ watcher_key ]

		watcher.PublishToPage( False )
		watcher.Repage( 'dead page key' )

		self._watchers.remove( watcher )

		self._last_time_watchers_changed = GetNowPrecise()

		del self._watcher_keys_to_watchers[ watcher_key ]

	def _SetDirty( self ):
		self._status_dirty = True

	def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
		if version == 1:
			serialisable_watchers = old_serialisable_info

			try:
				checker_options = HG.client_controller.new_options.GetDefaultWatcherCheckerOptions()
				file_import_options = HG.client_controller.new_options.GetDefaultFileImportOptions( 'loud' )
				tag_import_options = TagImportOptions( is_default = True )

			except Exception:
				checker_options = CheckerOptions()
				file_import_options = FileImportOptions()
				tag_import_options = TagImportOptions()

			serialisable_checker_options = checker_options.GetSerialisableTuple()
			serialisable_file_import_options = file_import_options.GetSerialisableTuple()
			serialisable_tag_import_options = tag_import_options.GetSerialisableTuple()

			highlighted_watcher_key = None

			serialisable_highlighted_watcher_key = highlighted_watcher_key

			new_serialisable_info = ( serialisable_watchers, serialisable_highlighted_watcher_key, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options )

			return ( 2, new_serialisable_info )

	def AddURL( self, url, service_keys_to_tags = None ):
		if url == '':
			return None

		url = HG.client_controller.network_engine.domain_manager.NormaliseURL( url )

		with self._lock:
			for watcher in self._watchers:
				if url == watcher.GetURL():
					watcher_key = watcher.GetWatcherKey()

					self._watcher_keys_to_already_in_timestamps[ watcher_key ] = GetNow()

					return None

			watcher = WatcherImport()

			watcher.SetURL( url )

			if service_keys_to_tags is not None:
				watcher.SetFixedServiceKeysToTags( service_keys_to_tags )

			watcher.SetCheckerOptions( self._checker_options )
			watcher.SetFileImportOptions( self._file_import_options )
			watcher.SetTagImportOptions( self._tag_import_options )

			publish_to_page = False

			watcher.Start( self._page_key, publish_to_page )

			self._AddWatcher( watcher )

		return watcher

	def AddWatcher( self, watcher ):
		with self._lock:
			self._AddWatcher( watcher )

			self._SetDirty()

	def GetHighlightedWatcher( self ):
		with self._lock:
			if self._highlighted_watcher_url is not None:
				for watcher in self._watchers:
					if watcher.GetURL() == self._highlighted_watcher_url:
						return watcher

				self._highlighted_watcher_url = None

			return None

	def GetLastTimeWatchersChanged( self ):
		with self._lock:
			return self._last_time_watchers_changed

	def GetNumDead( self ):
		with self._lock:
			return len( [ watcher for watcher in self._watchers if watcher.IsDead() ] )

	def GetNumWatchers( self ):
		with self._lock:
			return len( self._watchers )

	def GetOptions( self ):
		with self._lock:
			return ( self._checker_options, self._file_import_options, self._tag_import_options )

	def GetTotalStatus( self ):
		with self._lock:
			if self._status_dirty:
				self._RegenerateStatus()

			return self._status_cache

	def GetValueRange( self ):
		with self._lock:
			total_value = 0
			total_range = 0

			for watcher in self._watchers:
				( value, range ) = watcher.GetValueRange()

				if value != range:
					total_value += value
					total_range += range

			return ( total_value, total_range )

	def GetWatchers( self ):
		with self._lock:
			return list( self._watchers )

	def GetWatcherSimpleStatus( self, watcher ):
		with self._lock:
			watcher_key = watcher.GetWatcherKey()

			if watcher_key in self._watcher_keys_to_added_timestamps:
				added_timestamp = self._watcher_keys_to_added_timestamps[ watcher_key ]

				if TimeHasPassed( added_timestamp + self.ADDED_TIMESTAMP_DURATION ):
					self._CleanAddedTimestamps()

				else:
					return 'just added'

			if watcher_key in self._watcher_keys_to_already_in_timestamps:
				already_in_timestamp = self._watcher_keys_to_already_in_timestamps[ watcher_key ]

				if TimeHasPassed( already_in_timestamp + self.ADDED_TIMESTAMP_DURATION ):
					self._CleanAddedTimestamps()

				else:
					return 'already watching'

		return watcher.GetSimpleStatus()

	def RemoveWatcher( self, watcher_key ):
		with self._lock:
			self._RemoveWatcher( watcher_key )

			self._SetDirty()

	def SetHighlightedWatcher( self, highlighted_watcher ):
		with self._lock:
			if highlighted_watcher is None:
				self._highlighted_watcher_url = None

			else:
				self._highlighted_watcher_url = highlighted_watcher.GetURL()

				highlighted_watcher.PublishToPage( True )

	def SetOptions( self, checker_options, file_import_options, tag_import_options ):
		with self._lock:
			self._checker_options = checker_options
			self._file_import_options = file_import_options
			self._tag_import_options = tag_import_options

	def Start( self, page_key ):
		with self._lock:
			self._page_key = page_key

		# set a 2s period so the page value/range is breddy snappy
		self._watchers_repeating_job = HG.client_controller.CallRepeating( GetRepeatingJobInitialDelay(), 2.0, self.REPEATINGWorkOnWatchers )

		for watcher in self._watchers:
			publish_to_page = False

			if self._highlighted_watcher_url is not None and watcher.GetURL() == self._highlighted_watcher_url:
				publish_to_page = True

			watcher.Start( page_key, publish_to_page )

	def REPEATINGWorkOnWatchers( self ):
		with self._lock:
			if PageImporterShouldStopWorking( self._page_key ):
				self._watchers_repeating_job.Cancel()

				return

			if not self._status_dirty: # if we think we are clean

				for watcher in self._watchers:
					file_seed_cache = watcher.GetFileSeedCache()

					if file_seed_cache.GetStatusGenerationTime() > self._status_cache_generation_time: # has there has been an update?

						self._SetDirty()

						break

		if TimeHasPassed( self._next_pub_value_check_time ):
			self._next_pub_value_check_time = GetNow() + 5

			current_value_range = self.GetValueRange()

			if current_value_range != self._last_pubbed_value_range:
				self._last_pubbed_value_range = current_value_range

				HG.client_controller.pub( 'refresh_page_name', self._page_key )

		# something like:
			# if any are dead, do some stuff with them based on some options here
			# might want to have this work on a 30s period or something

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["MULTIPLE_WATCHER_IMPORT"] ] = MultipleWatcherImport

class WatcherImport( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["WATCHER_IMPORT"]
	SERIALISABLE_NAME = 'Watcher'
	SERIALISABLE_VERSION = 7

	MIN_CHECK_PERIOD = 30

	def __init__( self ):
		SerialisableBase.__init__( self )

		self._page_key = 'initialising page key'
		self._publish_to_page = False

		self._url = ''

		self._gallery_seed_log = GallerySeedLog()
		self._file_seed_cache = FileSeedCache()

		self._fixed_service_keys_to_tags = ServiceKeysToTags()

		self._checker_options = HG.client_controller.new_options.GetDefaultWatcherCheckerOptions()
		self._file_import_options = HG.client_controller.new_options.GetDefaultFileImportOptions( 'loud' )
		self._tag_import_options = TagImportOptions( is_default = True )
		self._last_check_time = 0
		self._checking_status = CHECKER_STATUS_OK
		self._subject = 'unknown subject'

		self._next_check_time = None

		self._file_network_job = None
		self._checker_network_job = None

		self._check_now = False
		self._files_paused = False
		self._checking_paused = False

		self._no_work_until = 0
		self._no_work_until_reason = ''

		self._creation_time = GetNow()

		self._file_velocity_status = ''
		self._file_status = ''
		self._watcher_status = ''

		self._watcher_key = GenerateKey()

		self._lock = threading.Lock()

		self._last_pubbed_page_name = ''

		self._files_repeating_job = None
		self._checker_repeating_job = None

		HG.client_controller.sub( self, 'NotifyFileSeedsUpdated', 'file_seed_cache_file_seeds_updated' )

	def _CheckerNetworkJobPresentationContextFactory( self, network_job ):
		def enter_call():
			with self._lock:
				self._checker_network_job = network_job

		def exit_call():
			with self._lock:
				self._checker_network_job = None

		return NetworkJobPresentationContext( enter_call, exit_call )

	def _CheckWatchableURL( self ):
		def file_seeds_callable( file_seeds ):
			return UpdateFileSeedCacheWithFileSeeds( self._file_seed_cache, file_seeds )

		def status_hook( text ):
			with self._lock:
				self._watcher_status = text

		def title_hook( text ):
			with self._lock:
				self._subject = text

		gallery_seed = GallerySeed( self._url, can_generate_more_pages = False )

		gallery_seed.SetFixedServiceKeysToTags( self._fixed_service_keys_to_tags )

		self._gallery_seed_log.AddGallerySeeds( ( gallery_seed, ) )

		with self._lock:
			self._watcher_status = 'checking'

		try:
			( num_urls_added, num_urls_already_in_file_seed_cache, num_urls_total, result_404, added_new_gallery_pages, stop_reason ) = gallery_seed.WorkOnURL( 'watcher', self._gallery_seed_log, file_seeds_callable, status_hook, title_hook, self._NetworkJobFactory, self._CheckerNetworkJobPresentationContextFactory, self._file_import_options )

			if num_urls_added > 0:
				WakeRepeatingJob( self._files_repeating_job )

			if result_404:
				with self._lock:
					self._checking_paused = True

					self._checking_status = CHECKER_STATUS_404

			if gallery_seed.status == CC.STATUS_ERROR:
				# the [DEAD] stuff can override watcher status, so let's give a brief time for this to display the error

				with self._lock:
					self._checking_paused = True

					self._watcher_status = gallery_seed.note

				time.sleep( 5 )

		except NetworkException as e:
			delay = HG.client_controller.new_options.GetInteger( 'downloader_network_error_delay' )

			self._DelayWork( delay, str( e ) )

			PrintException( e )

		watcher_status = gallery_seed.note
		watcher_status_should_stick = gallery_seed.status != CC.STATUS_SUCCESSFUL_AND_NEW

		with self._lock:
			if self._check_now:
				self._check_now = False

			self._watcher_status = watcher_status

			self._last_check_time = GetNow()

			self._UpdateFileVelocityStatus()

			self._UpdateNextCheckTime()

			self._Compact()

		if not watcher_status_should_stick:
			time.sleep( 5 )

			with self._lock:
				self._watcher_status = ''

	def _Compact( self ):
		death_period = self._checker_options.GetDeathFileVelocityPeriod()

		compact_before_this_time = self._last_check_time - ( death_period * 2 )

		self._gallery_seed_log.Compact( compact_before_this_time )

	def _DelayWork( self, time_delta, reason ):
		self._no_work_until = GetNow() + time_delta
		self._no_work_until_reason = reason

	def _FileNetworkJobPresentationContextFactory( self, network_job ):
		def enter_call():
			with self._lock:
				self._file_network_job = network_job

		def exit_call():
			with self._lock:
				self._file_network_job = None

		return NetworkJobPresentationContext( enter_call, exit_call )

	def _NetworkJobFactory( self, *args, **kwargs ):
		network_job = NetworkJobWatcherPage( self._watcher_key, *args, **kwargs )

		return network_job

	def _GetSerialisableInfo( self ):
		serialisable_gallery_seed_log = self._gallery_seed_log.GetSerialisableTuple()
		serialisable_file_seed_cache = self._file_seed_cache.GetSerialisableTuple()

		serialisable_fixed_service_keys_to_tags = self._fixed_service_keys_to_tags.GetSerialisableTuple()

		serialisable_checker_options = self._checker_options.GetSerialisableTuple()
		serialisable_file_import_options = self._file_import_options.GetSerialisableTuple()
		serialisable_tag_import_options = self._tag_import_options.GetSerialisableTuple()

		return ( self._url, serialisable_gallery_seed_log, serialisable_file_seed_cache, serialisable_fixed_service_keys_to_tags, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, self._last_check_time, self._files_paused, self._checking_paused, self._checking_status, self._subject, self._no_work_until, self._no_work_until_reason, self._creation_time )

	def _HasURL( self ):
		return self._url != ''

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( self._url, serialisable_gallery_seed_log, serialisable_file_seed_cache, serialisable_fixed_service_keys_to_tags, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, self._last_check_time, self._files_paused, self._checking_paused, self._checking_status, self._subject, self._no_work_until, self._no_work_until_reason, self._creation_time ) = serialisable_info

		self._fixed_service_keys_to_tags = CreateFromSerialisableTuple( serialisable_fixed_service_keys_to_tags )

		self._gallery_seed_log = CreateFromSerialisableTuple( serialisable_gallery_seed_log )
		self._file_seed_cache = CreateFromSerialisableTuple( serialisable_file_seed_cache )

		self._checker_options = CreateFromSerialisableTuple( serialisable_checker_options )
		self._file_import_options = CreateFromSerialisableTuple( serialisable_file_import_options )
		self._tag_import_options = CreateFromSerialisableTuple( serialisable_tag_import_options )

	def _UpdateFileVelocityStatus( self ):
		self._file_velocity_status = self._checker_options.GetPrettyCurrentVelocity( self._file_seed_cache, self._last_check_time )

	def _UpdateNextCheckTime( self ):
		if self._check_now:
			self._next_check_time = self._last_check_time + self.MIN_CHECK_PERIOD

		else:
			if not TimeHasPassed( self._no_work_until ):
				self._next_check_time = self._no_work_until + 1

			else:
				if self._checking_status == CHECKER_STATUS_OK:
					if self._checker_options.IsDead( self._file_seed_cache, self._last_check_time ):
						self._checking_status = CHECKER_STATUS_DEAD

				if self._checking_status != CHECKER_STATUS_OK:
					self._checking_paused = True

				last_next_check_time = self._next_check_time

				self._next_check_time = self._checker_options.GetNextCheckTime( self._file_seed_cache, self._last_check_time, last_next_check_time )

	def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
		if version == 1:
			( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_file_import_options, serialisable_tag_import_options, times_to_check, check_period, last_check_time, paused ) = old_serialisable_info

			checker_options = CheckerOptions( intended_files_per_check = 8, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )

			serialisable_checker_options = checker_options.GetSerialisableTuple()

			files_paused = paused
			checking_paused = paused

			new_serialisable_info = ( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused )

			return ( 2, new_serialisable_info )

		if version == 2:
			( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused ) = old_serialisable_info

			checking_status = CHECKER_STATUS_OK
			subject = 'unknown subject'

			new_serialisable_info = ( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject )

			return ( 3, new_serialisable_info )

		if version == 3:
			( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject ) = old_serialisable_info

			no_work_until = 0
			no_work_until_reason = ''

			new_serialisable_info = ( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject, no_work_until, no_work_until_reason )

			return ( 4, new_serialisable_info )

		if version == 4:
			( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject, no_work_until, no_work_until_reason ) = old_serialisable_info

			creation_time = GetNow()

			new_serialisable_info = ( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject, no_work_until, no_work_until_reason, creation_time )

			return ( 5, new_serialisable_info )

		if version == 5:
			( url, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject, no_work_until, no_work_until_reason, creation_time ) = old_serialisable_info

			gallery_seed_log = GallerySeedLog()

			serialisable_gallery_seed_log = gallery_seed_log.GetSerialisableTuple()

			new_serialisable_info = ( url, serialisable_gallery_seed_log, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject, no_work_until, no_work_until_reason, creation_time )

			return ( 6, new_serialisable_info )

		if version == 6:
			( url, serialisable_gallery_seed_log, serialisable_file_seed_cache, urls_to_filenames, urls_to_md5_base64, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject, no_work_until, no_work_until_reason, creation_time ) = old_serialisable_info

			fixed_service_keys_to_tags = ServiceKeysToTags()

			serialisable_fixed_service_keys_to_tags = fixed_service_keys_to_tags.GetSerialisableTuple()

			new_serialisable_info = ( url, serialisable_gallery_seed_log, serialisable_file_seed_cache, serialisable_fixed_service_keys_to_tags, serialisable_checker_options, serialisable_file_import_options, serialisable_tag_import_options, last_check_time, files_paused, checking_paused, checking_status, subject, no_work_until, no_work_until_reason, creation_time )

			return ( 7, new_serialisable_info )

	def _WorkOnFiles( self ):
		file_seed = self._file_seed_cache.GetNextFileSeed( CC.STATUS_UNKNOWN )

		if file_seed is None:
			return

		did_substantial_work = False

		def status_hook( text ):
			with self._lock:
				self._file_status = text

		did_substantial_work = file_seed.WorkOnURL( self._file_seed_cache, status_hook, self._NetworkJobFactory, self._FileNetworkJobPresentationContextFactory, self._file_import_options, self._tag_import_options )

		with self._lock:
			should_present = self._publish_to_page and file_seed.ShouldPresent( self._file_import_options )

			page_key = self._page_key

		if should_present:
			file_seed.PresentToPage( page_key )

			did_substantial_work = True

		with self._lock:
			self._file_status = ''

		if did_substantial_work:
			time.sleep( DID_SUBSTANTIAL_FILE_WORK_MINIMUM_SLEEP_TIME )

	def CanRetryFailed( self ):
		with self._lock:
			return self._file_seed_cache.GetFileSeedCount( CC.STATUS_ERROR ) > 0

	def CheckingPaused( self ):
		with self._lock:
			return self._checking_paused

	def CheckNow( self ):
		with self._lock:
			self._check_now = True

			self._checking_paused = False

			self._no_work_until = 0
			self._no_work_until_reason = ''

			self._checking_status = CHECKER_STATUS_OK

			self._UpdateNextCheckTime()

			WakeRepeatingJob( self._checker_repeating_job )

	def CurrentlyAlive( self ):
		with self._lock:
			return self._checking_status == CHECKER_STATUS_OK

	def CurrentlyWorking( self ):
		with self._lock:
			finished = not self._file_seed_cache.WorkToDo()

			return not finished and not self._files_paused

	def FilesPaused( self ):
		with self._lock:
			return self._files_paused

	def GetCheckerOptions( self ):
		with self._lock:
			return self._checker_options

	def GetCreationTime( self ):
		with self._lock:
			return self._creation_time

	def GetFileImportOptions( self ):
		with self._lock:
			return self._file_import_options

	def GetFileSeedCache( self ):
		with self._lock:
			return self._file_seed_cache

	def GetGallerySeedLog( self ):
		with self._lock:
			return self._gallery_seed_log

	def GetHashes( self ):
		with self._lock:
			return self._file_seed_cache.GetHashes()

	def GetNetworkJobs( self ):
		with self._lock:
			return ( self._file_network_job, self._checker_network_job )

	def GetNewHashes( self ):
		with self._lock:
			file_import_options = FileImportOptions()

			file_import_options.SetPresentationOptions( True, False, False )

			return self._file_seed_cache.GetPresentedHashes( file_import_options )

	def GetOptions( self ):
		with self._lock:
			return ( self._url, self._file_import_options, self._tag_import_options )

	def GetPresentedHashes( self ):
		with self._lock:
			return self._file_seed_cache.GetPresentedHashes( self._file_import_options )

	def GetSimpleStatus( self ):
		with self._lock:
			if self._checking_status == CHECKER_STATUS_404:
				return '404'

			elif self._checking_status == CHECKER_STATUS_DEAD:
				return 'DEAD'

			elif not TimeHasPassed( self._no_work_until ):
				return self._no_work_until_reason + ' - ' + 'next check ' + TimestampToPrettyTimeDelta( self._next_check_time )

			else:
				return ''

	def GetStatus( self ):
		with self._lock:
			file_status = self._file_status

			if self._checking_status == CHECKER_STATUS_404:
				watcher_status = 'URL 404'

			elif self._checking_status == CHECKER_STATUS_DEAD:
				watcher_status = 'URL DEAD'

			elif not TimeHasPassed( self._no_work_until ):
				no_work_text = self._no_work_until_reason + ' - ' + 'next check ' + TimestampToPrettyTimeDelta( self._next_check_time )

				file_status = no_work_text
				watcher_status = no_work_text

			else:
				watcher_status = self._watcher_status

			return ( file_status, self._files_paused, self._file_velocity_status, self._next_check_time, watcher_status, self._subject, self._checking_status, self._check_now, self._checking_paused )

	def GetSubject( self ):
		with self._lock:
			if self._subject in ( None, '' ):
				return 'unknown subject'

			else:
				return self._subject

	def GetTagImportOptions( self ):
		with self._lock:
			return self._tag_import_options

	def GetWatcherKey( self ):
		with self._lock:
			return self._watcher_key

	def GetURL( self ):
		with self._lock:
			return self._url

	def GetValueRange( self ):
		with self._lock:
			return self._file_seed_cache.GetValueRange()

	def HasURL( self ):
		with self._lock:
			return self._HasURL()

	def _IsDead( self ):
		return self._checking_status in ( CHECKER_STATUS_404, CHECKER_STATUS_DEAD )

	def IsDead( self ):
		with self._lock:
			return self._IsDead()

	def NotifyFileSeedsUpdated( self, file_seed_cache_key, file_seeds ):
		if file_seed_cache_key == self._file_seed_cache.GetFileSeedCacheKey():
			WakeRepeatingJob( self._files_repeating_job )

	def PausePlayChecking( self ):
		with self._lock:
			if self._checking_paused and self._IsDead():
				return # watcher is dead, so don't unpause until a checknow event

			else:
				self._checking_paused = not self._checking_paused

				WakeRepeatingJob( self._checker_repeating_job )

	def PausePlayFiles( self ):
		with self._lock:
			self._files_paused = not self._files_paused

			WakeRepeatingJob( self._files_repeating_job )

	def PublishToPage( self, publish_to_page ):
		with self._lock:
			self._publish_to_page = publish_to_page

	def Repage( self, page_key ):
		with self._lock:
			self._page_key = page_key

	def RetryFailed( self ):
		with self._lock:
			self._file_seed_cache.RetryFailures()

	def SetCheckerOptions( self, checker_options ):
		with self._lock:
			self._checker_options = checker_options

			self._UpdateNextCheckTime()

			self._UpdateFileVelocityStatus()

			WakeRepeatingJob( self._checker_repeating_job )

	def SetFileImportOptions( self, file_import_options ):
		with self._lock:
			self._file_import_options = file_import_options

	def SetFixedServiceKeysToTags( self, service_keys_to_tags ):
		with self._lock:
			self._fixed_service_keys_to_tags = ServiceKeysToTags( service_keys_to_tags )

	def SetTagImportOptions( self, tag_import_options ):
		with self._lock:
			self._tag_import_options = tag_import_options

	def SetURL( self, url ):
		if url is None:
			url = ''

		if url != '':
			url = HG.client_controller.network_engine.domain_manager.NormaliseURL( url )

		with self._lock:
			self._url = url

			WakeRepeatingJob( self._checker_repeating_job )

	def Start( self, page_key, publish_to_page ):
		self._page_key = page_key
		self._publish_to_page = publish_to_page

		self._UpdateNextCheckTime()

		self._UpdateFileVelocityStatus()

		self._files_repeating_job = HG.client_controller.CallRepeating( GetRepeatingJobInitialDelay(), REPEATING_JOB_TYPICAL_PERIOD, self.REPEATINGWorkOnFiles )
		self._checker_repeating_job = HG.client_controller.CallRepeating( GetRepeatingJobInitialDelay(), REPEATING_JOB_TYPICAL_PERIOD, self.REPEATINGWorkOnChecker )

		self._files_repeating_job.SetThreadSlotType( 'watcher_files' )
		self._checker_repeating_job.SetThreadSlotType( 'watcher_check' )

	def REPEATINGWorkOnFiles( self ):
		with self._lock:
			if PageImporterShouldStopWorking( self._page_key ):
				self._files_repeating_job.Cancel()

				return

			files_paused = self._files_paused or HG.client_controller.new_options.GetBoolean( 'pause_all_file_queues' )
			work_pending = self._file_seed_cache.WorkToDo() and not files_paused
			no_delays = TimeHasPassed( self._no_work_until )
			page_shown = not HG.client_controller.PageClosedButNotDestroyed( self._page_key )
			network_engine_good = not HG.client_controller.network_engine.IsBusy()

			ok_to_work = work_pending and no_delays and page_shown and network_engine_good

		while ok_to_work:
			try:
				self._WorkOnFiles()

				HG.client_controller.WaitUntilViewFree()

			except Exception as e:
				ShowException( e )

			with self._lock:
				if PageImporterShouldStopWorking( self._page_key ):
					self._files_repeating_job.Cancel()

					return

				files_paused = self._files_paused or HG.client_controller.new_options.GetBoolean( 'pause_all_file_queues' )
				work_pending = self._file_seed_cache.WorkToDo() and not files_paused
				no_delays = TimeHasPassed( self._no_work_until )
				page_shown = not HG.client_controller.PageClosedButNotDestroyed( self._page_key )
				network_engine_good = not HG.client_controller.network_engine.IsBusy()

				ok_to_work = work_pending and no_delays and page_shown and network_engine_good

	def REPEATINGWorkOnChecker( self ):
		with self._lock:
			if PageImporterShouldStopWorking( self._page_key ):
				self._checker_repeating_job.Cancel()

				return

			checking_paused = self._checking_paused or HG.client_controller.new_options.GetBoolean( 'pause_all_watcher_checkers' )

			able_to_check = self._checking_status == CHECKER_STATUS_OK and self._HasURL() and not checking_paused
			check_due = TimeHasPassed( self._next_check_time )
			no_delays = TimeHasPassed( self._no_work_until )
			page_shown = not HG.client_controller.PageClosedButNotDestroyed( self._page_key )
			network_engine_good = not HG.client_controller.network_engine.IsBusy()

			time_to_check = able_to_check and check_due and no_delays and page_shown and network_engine_good

		if time_to_check:
			try:
				self._CheckWatchableURL()

			except Exception as e:
				ShowException( e )

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["WATCHER_IMPORT"] ] = WatcherImport


code:
from .Import import *

__all__ = [
	"FileSeeds",
	"Gallery",
	"GallerySeeds",
	"Local",
	"Options",
	"SimpleURLs",
	"Subscriptions",
	"Watchers"
]

from . import *

license: wtfpl


code:
import re, time, datetime

from lib.Hydrus.Data import ConvertIntToPixels, ConvertIntToUnit, ConvertMillisecondsToPrettyTime, \
	ConvertTimestampToPrettyTime, ConvertValueRangeToPrettyString, GetNow, TimeDeltaToPrettyTimeDelta, \
	ToHumanInt
from lib.Hydrus.Data.Local import MergeCounts
from lib.Hydrus.Exceptions import DataMissing
from lib.Hydrus.Serialisable import CreateFromSerialisableTuple, SerialisableBase, SerialisableList
from lib.Hydrus.Tags import CleanTag, CombineTag, SplitTag
from lib.Hydrus.Tags.Local import RenderTag

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants as HC
import lib.Hydrus.Constants.Local as CC

from lib.Hydrus.Serialisable import HG.SerialisableType["FILE_SEARCH_CONTEXT"], \
	HG.SerialisableType["PREDICATE"], SERIALISABLE_TYPES_TO_OBJECT_TYPES

IGNORED_TAG_SEARCH_CHARACTERS = '[](){}"\''
IGNORED_TAG_SEARCH_CHARACTERS_UNICODE_TRANSLATE = { ord( char ) : None for char in IGNORED_TAG_SEARCH_CHARACTERS }

### FUNCTIONS ###

def ConvertTagToSearchable( tag ):
	if tag == '':
		return ''

	while '**' in tag:
		tag = tag.replace( '**', '*' )

	if IsComplexWildcard( tag ):
		return tag

	tag = tag.translate( IGNORED_TAG_SEARCH_CHARACTERS_UNICODE_TRANSLATE )

	return tag

def ConvertEntryTextToSearchText( entry_text ):
	entry_text = CleanTag( entry_text )

	entry_text = ConvertTagToSearchable( entry_text )

	if not IsComplexWildcard( entry_text ) and not entry_text.endswith( '*' ):
		entry_text = entry_text + '*'

	return entry_text

def FilterPredicatesBySearchText( service_key, search_text, predicates ):
	tags_to_predicates = {}

	for predicate in predicates:
		( predicate_type, value, inclusive ) = predicate.GetInfo()

		if predicate_type == HC.PREDICATE_TYPE_TAG:
			tags_to_predicates[ value ] = predicate

	matching_tags = FilterTagsBySearchText( service_key, search_text, list( tags_to_predicates.keys() ) )

	matches = [ tags_to_predicates[ tag ] for tag in matching_tags ]

	return matches

def FilterTagsBySearchText( service_key, search_text, tags, search_siblings = True ):
	def compile_re( s ):
		regular_parts_of_s = s.split( '*' )

		escaped_parts_of_s = list( map( re.escape, regular_parts_of_s ) )

		s = '.*'.join( escaped_parts_of_s )

		# \A is start of string
		# \Z is end of string
		# \s is whitespace

		if s.startswith( '.*' ):
			beginning = '(\\A|:)'

		else:
			beginning = '(\\A|:|\\s)'

		if s.endswith( '.*' ):
			end = '\\Z' # end of string

		else:
			end = '(\\s|\\Z)' # whitespace or end of string

		return re.compile( beginning + s + end )

	re_predicate = compile_re( search_text )

	sibling_manager = HG.client_controller.tag_siblings_manager

	result = []

	for tag in tags:
		if search_siblings:
			possible_tags = sibling_manager.GetAllSiblings( service_key, tag )

		else:
			possible_tags = [ tag ]

		if not IsComplexWildcard( search_text ):
			possible_tags = list( map( ConvertTagToSearchable, possible_tags ) )

		for possible_tag in possible_tags:
			if re_predicate.search( possible_tag ) is not None:
				result.append( tag )

				break

	return result

def IsComplexWildcard( search_text ):
	num_stars = search_text.count( '*' )

	if num_stars > 1:
		return True

	if num_stars == 1 and not search_text.endswith( '*' ):
		return True

	return False

def SortPredicates( predicates ):
	key = lambda p: p.GetCount()

	predicates.sort( key = key, reverse = True )

	return predicates

SEARCH_TYPE_AND = 0
SEARCH_TYPE_OR = 1

class FileSearchContext( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["FILE_SEARCH_CONTEXT"]
	SERIALISABLE_NAME = 'File Search Context'
	SERIALISABLE_VERSION = 3

	def __init__( self, file_service_key = CC.COMBINED_FILE_SERVICE_KEY, tag_service_key = CC.COMBINED_TAG_SERVICE_KEY, search_type = SEARCH_TYPE_AND, include_current_tags = True, include_pending_tags = True, predicates = None ):
		if predicates is None:
			predicates = []

		self._file_service_key = file_service_key
		self._tag_service_key = tag_service_key

		self._search_type = search_type

		self._include_current_tags = include_current_tags
		self._include_pending_tags = include_pending_tags

		self._predicates = predicates

		self._search_complete = False

		self._InitialiseTemporaryVariables()

	def _GetSerialisableInfo( self ):
		serialisable_predicates = [ predicate.GetSerialisableTuple() for predicate in self._predicates ]

		return ( self._file_service_key.hex(), self._tag_service_key.hex(), self._search_type, self._include_current_tags, self._include_pending_tags, serialisable_predicates, self._search_complete )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( file_service_key, tag_service_key, self._search_type, self._include_current_tags, self._include_pending_tags, serialisable_predicates, self._search_complete ) = serialisable_info

		self._file_service_key = bytes.fromhex( file_service_key )
		self._tag_service_key = bytes.fromhex( tag_service_key )

		services_manager = HG.client_controller.services_manager

		if not services_manager.ServiceExists( self._file_service_key ):
			self._file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY

		if not services_manager.ServiceExists( self._tag_service_key ):
			self._tag_service_key = CC.COMBINED_TAG_SERVICE_KEY

		self._predicates = [ CreateFromSerialisableTuple( pred_tuple ) for pred_tuple in serialisable_predicates ]

		self._InitialiseTemporaryVariables()

	def _InitialiseTemporaryVariables( self ):
		system_predicates = [ predicate for predicate in self._predicates if predicate.GetType() in HC.SYSTEM_PREDICATES ]

		self._system_predicates = FileSystemPredicates( system_predicates )

		tag_predicates = [ predicate for predicate in self._predicates if predicate.GetType() == HC.PREDICATE_TYPE_TAG ]

		self._tags_to_include = []
		self._tags_to_exclude = []

		for predicate in tag_predicates:
			tag = predicate.GetValue()

			if predicate.GetInclusive():
				self._tags_to_include.append( tag )
			else:
				self._tags_to_exclude.append( tag )

		namespace_predicates = [ predicate for predicate in self._predicates if predicate.GetType() == HC.PREDICATE_TYPE_NAMESPACE ]

		self._namespaces_to_include = []
		self._namespaces_to_exclude = []

		for predicate in namespace_predicates:
			namespace = predicate.GetValue()

			if predicate.GetInclusive():
				self._namespaces_to_include.append( namespace )
			else:
				self._namespaces_to_exclude.append( namespace )

		wildcard_predicates = [ predicate for predicate in self._predicates if predicate.GetType() == HC.PREDICATE_TYPE_WILDCARD ]

		self._wildcards_to_include = []
		self._wildcards_to_exclude = []

		for predicate in wildcard_predicates:
			wildcard = predicate.GetValue()

			if predicate.GetInclusive():
				self._wildcards_to_include.append( wildcard )
			else:
				self._wildcards_to_exclude.append( wildcard )

		self._or_predicates = [ predicate for predicate in self._predicates if predicate.GetType() == HC.PREDICATE_TYPE_OR_CONTAINER ]

	def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
		if version == 1:
			( file_service_key_hex, tag_service_key_hex, include_current_tags, include_pending_tags, serialisable_predicates, search_complete ) = old_serialisable_info

			search_type = SEARCH_TYPE_AND

			new_serialisable_info = ( file_service_key_hex, tag_service_key_hex, search_type, include_current_tags, include_pending_tags, serialisable_predicates, search_complete )

			return ( 2, new_serialisable_info )

		if version == 2:
			( file_service_key_hex, tag_service_key_hex, search_type, include_current_tags, include_pending_tags, serialisable_predicates, search_complete ) = old_serialisable_info

			# screwed up the serialisation code for the previous update, so these were getting swapped

			search_type = SEARCH_TYPE_AND
			include_current_tags = True

			new_serialisable_info = ( file_service_key_hex, tag_service_key_hex, search_type, include_current_tags, include_pending_tags, serialisable_predicates, search_complete )

			return ( 3, new_serialisable_info )

	def GetFileServiceKey( self ): return self._file_service_key
	def GetNamespacesToExclude( self ): return self._namespaces_to_exclude

	def GetNamespacesToInclude( self ): return self._namespaces_to_include
	def GetORPredicates( self ): return self._or_predicates

	def GetPredicates( self ): return self._predicates
	def GetSystemPredicates( self ): return self._system_predicates

	def GetTagServiceKey( self ): return self._tag_service_key
	def GetTagsToExclude( self ): return self._tags_to_exclude

	def GetTagsToInclude( self ): return self._tags_to_include
	def GetWildcardsToExclude( self ): return self._wildcards_to_exclude

	def GetWildcardsToInclude( self ): return self._wildcards_to_include

	def HasNoPredicates( self ):
		return len( self._predicates ) == 0

	def IncludeCurrentTags( self ): return self._include_current_tags
	def IncludePendingTags( self ): return self._include_pending_tags

	def IsComplete( self ): return self._search_complete

	def IsJustSystemEverything( self ):
		return len( self._predicates ) == 1 and self._system_predicates.HasSystemEverything()

	def SetComplete( self ): self._search_complete = True

	def SetFileServiceKey( self, file_service_key ):
		self._file_service_key = file_service_key

	def SetIncludeCurrentTags( self, value ):
		self._include_current_tags = value

	def SetIncludePendingTags( self, value ):
		self._include_pending_tags = value

	def SetPredicates( self, predicates ):
		self._predicates = predicates

		self._InitialiseTemporaryVariables()

	def SetTagServiceKey( self, tag_service_key ):
		self._tag_service_key = tag_service_key

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["FILE_SEARCH_CONTEXT"] ] = FileSearchContext

class FileSystemPredicates( object ):
	def __init__( self, system_predicates, apply_implicit_limit = True ):
		self._has_system_everything = False

		self._inbox = False
		self._archive = False
		self._local = False
		self._not_local = False

		self._common_info = {}

		self._limit = None
		self._similar_to = None

		self._file_services_to_include_current = []
		self._file_services_to_include_pending = []
		self._file_services_to_exclude_current = []
		self._file_services_to_exclude_pending = []

		self._ratings_predicates = []

		self._duplicate_predicates = []

		self._file_viewing_stats_predicates = []

		# new_options = HG.client_controller.new_options

		for predicate in system_predicates:
			predicate_type = predicate.GetType()
			value = predicate.GetValue()

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_EVERYTHING:
				self._has_system_everything = True
			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_INBOX:
				self._inbox = True
			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_ARCHIVE:
				self._archive = True
			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_LOCAL:
				self._local = True
			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_NOT_LOCAL:
				self._not_local = True

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS:
				( operator, rule_type, rule, description ) = value

				if 'known_url_rules' not in self._common_info:
					self._common_info[ 'known_url_rules' ] = []

				self._common_info[ 'known_url_rules' ].append( ( operator, rule_type, rule ) )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_HASH:
				( hash, hash_type ) = value

				self._common_info[ 'hash' ] = ( hash, hash_type )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_AGE:
				( operator, age_type, age_value ) = value

				if age_type == 'delta':
					( years, months, days, hours ) = age_value

					age = ( ( ( ( ( ( ( years * 12 ) + months ) * 30 ) + days ) * 24 ) + hours ) * 3600 )

					now = GetNow()

					# this is backwards (less than means min timestamp) because we are talking about age, not timestamp

					if operator == '<':
						self._common_info[ 'min_timestamp' ] = now - age

					elif operator == '>':
						self._common_info[ 'max_timestamp' ] = now - age

					elif operator == '\u2248':
						self._common_info[ 'min_timestamp' ] = now - int( age * 1.15 )
						self._common_info[ 'max_timestamp' ] = now - int( age * 0.85 )

				elif age_type == 'date':
					( year, month, day ) = age_value

					# convert this dt, which is in local time, to a gmt timestamp

					day_dt = datetime.datetime( year, month, day )
					timestamp = int( time.mktime( day_dt.timetuple() ) )

					if operator == '<':
						self._common_info[ 'max_timestamp' ] = timestamp

					elif operator == '>':
						self._common_info[ 'min_timestamp' ] = timestamp + 86400

					elif operator == '=':
						self._common_info[ 'min_timestamp' ] = timestamp
						self._common_info[ 'max_timestamp' ] = timestamp + 86400

					elif operator == '\u2248':
						self._common_info[ 'min_timestamp' ] = timestamp - 86400 * 30
						self._common_info[ 'max_timestamp' ] = timestamp + 86400 * 30

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_MIME:
				mimes = value

				if isinstance( mimes, int ):
					mimes = ( mimes, )

				self._common_info[ 'mimes' ] = mimes

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_DURATION:
				( operator, duration ) = value

				if operator == '<':
					self._common_info[ 'max_duration' ] = duration
				elif operator == '>':
					self._common_info[ 'min_duration' ] = duration
				elif operator == '=':
					self._common_info[ 'duration' ] = duration
				elif operator == '\u2248':
					if duration == 0:
						self._common_info[ 'duration' ] = 0

					else:
						self._common_info[ 'min_duration' ] = int( duration * 0.85 )
						self._common_info[ 'max_duration' ] = int( duration * 1.15 )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_RATING:
				( operator, value, service_key ) = value

				self._ratings_predicates.append( ( operator, value, service_key ) )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_RATIO:
				( operator, ratio_width, ratio_height ) = value

				if operator == '=':
					self._common_info[ 'ratio' ] = ( ratio_width, ratio_height )
				elif operator == 'wider than':
					self._common_info[ 'min_ratio' ] = ( ratio_width, ratio_height )

				elif operator == 'taller than':
					self._common_info[ 'max_ratio' ] = ( ratio_width, ratio_height )

				elif operator == '\u2248':
					self._common_info[ 'min_ratio' ] = ( ratio_width * 0.85, ratio_height )
					self._common_info[ 'max_ratio' ] = ( ratio_width * 1.15, ratio_height )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIZE:
				( operator, size, unit ) = value

				size = size * unit

				if operator == '<':
					self._common_info[ 'max_size' ] = size
				elif operator == '>':
					self._common_info[ 'min_size' ] = size
				elif operator == '=':
					self._common_info[ 'size' ] = size
				elif operator == '\u2248':
					self._common_info[ 'min_size' ] = int( size * 0.85 )
					self._common_info[ 'max_size' ] = int( size * 1.15 )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_NUM_TAGS:
				( operator, num_tags ) = value

				if operator == '<':
					self._common_info[ 'max_num_tags' ] = num_tags
				elif operator == '=':
					self._common_info[ 'num_tags' ] = num_tags
				elif operator == '>':
					self._common_info[ 'min_num_tags' ] = num_tags

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER:
				( namespace, operator, num ) = value

				if operator == '<':
					self._common_info[ 'max_tag_as_number' ] = ( namespace, num )
				elif operator == '>':
					self._common_info[ 'min_tag_as_number' ] = ( namespace, num )
				elif operator == '\u2248':
					self._common_info[ 'min_tag_as_number' ] = ( namespace, int( num * 0.85 ) )
					self._common_info[ 'max_tag_as_number' ] = ( namespace, int( num * 1.15 ) )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_WIDTH:
				( operator, width ) = value

				if operator == '<':
					self._common_info[ 'max_width' ] = width
				elif operator == '>':
					self._common_info[ 'min_width' ] = width
				elif operator == '=':
					self._common_info[ 'width' ] = width
				elif operator == '\u2248':
					if width == 0:
						self._common_info[ 'width' ] = 0
					else:
						self._common_info[ 'min_width' ] = int( width * 0.85 )
						self._common_info[ 'max_width' ] = int( width * 1.15 )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_NUM_PIXELS:
				( operator, num_pixels, unit ) = value

				num_pixels = num_pixels * unit

				if operator == '<':
					self._common_info[ 'max_num_pixels' ] = num_pixels
				elif operator == '>':
					self._common_info[ 'min_num_pixels' ] = num_pixels
				elif operator == '=':
					self._common_info[ 'num_pixels' ] = num_pixels
				elif operator == '\u2248':
					self._common_info[ 'min_num_pixels' ] = int( num_pixels * 0.85 )
					self._common_info[ 'max_num_pixels' ] = int( num_pixels * 1.15 )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_HEIGHT:
				( operator, height ) = value

				if operator == '<':
					self._common_info[ 'max_height' ] = height
				elif operator == '>':
					self._common_info[ 'min_height' ] = height
				elif operator == '=':
					self._common_info[ 'height' ] = height
				elif operator == '\u2248':
					if height == 0:
						self._common_info[ 'height' ] = 0
					else:
						self._common_info[ 'min_height' ] = int( height * 0.85 )
						self._common_info[ 'max_height' ] = int( height * 1.15 )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_NUM_WORDS:
				( operator, num_words ) = value

				if operator == '<':
					self._common_info[ 'max_num_words' ] = num_words
				elif operator == '>':
					self._common_info[ 'min_num_words' ] = num_words
				elif operator == '=':
					self._common_info[ 'num_words' ] = num_words
				elif operator == '\u2248':
					if num_words == 0:
						self._common_info[ 'num_words' ] = 0
					else:
						self._common_info[ 'min_num_words' ] = int( num_words * 0.85 )
						self._common_info[ 'max_num_words' ] = int( num_words * 1.15 )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_LIMIT:
				limit = value

				if self._limit is None:
					self._limit = limit

				else:
					self._limit = min( limit, self._limit )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_FILE_SERVICE:
				( operator, current_or_pending, service_key ) = value

				if operator is True:
					if current_or_pending == HC.CONTENT_STATUS_CURRENT:
						self._file_services_to_include_current.append( service_key )
					else:
						self._file_services_to_include_pending.append( service_key )

				else:
					if current_or_pending == HC.CONTENT_STATUS_CURRENT:
						self._file_services_to_exclude_current.append( service_key )
					else:
						self._file_services_to_exclude_pending.append( service_key )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIMILAR_TO:
				( hash, max_hamming ) = value

				self._similar_to = ( hash, max_hamming )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_DUPLICATE_RELATIONSHIPS:
				( operator, num_relationships, dupe_type ) = value

				self._duplicate_predicates.append( ( operator, num_relationships, dupe_type ) )

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS:
				( view_type, viewing_locations, operator, viewing_value ) = value

				self._file_viewing_stats_predicates.append( ( view_type, viewing_locations, operator, viewing_value ) )

	def GetDuplicateRelationshipsPredicates( self ):
		return self._duplicate_predicates

	def GetFileServiceInfo( self ):
		return ( self._file_services_to_include_current, self._file_services_to_include_pending, self._file_services_to_exclude_current, self._file_services_to_exclude_pending )

	def GetFileViewingStatsPredicates( self ):
		return self._file_viewing_stats_predicates

	def GetSimpleInfo( self ):
		return self._common_info

	def GetLimit( self, apply_implicit_limit = True ):
		if self._limit is None and apply_implicit_limit:
			forced_search_limit = HG.client_controller.new_options.GetNoneableInteger( 'forced_search_limit' )

			return forced_search_limit

		return self._limit

	def GetRatingsPredicates( self ): return self._ratings_predicates

	def GetSimilarTo( self ): return self._similar_to

	def HasSimilarTo( self ): return self._similar_to is not None

	def HasSystemEverything( self ):
		return self._has_system_everything

	def MustBeArchive( self ): return self._archive

	def MustBeInbox( self ): return self._inbox

	def MustBeLocal( self ): return self._local

	def MustNotBeLocal( self ): return self._not_local

class Predicate( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["PREDICATE"]
	SERIALISABLE_NAME = 'File Search Predicate'
	SERIALISABLE_VERSION = 2

	def __init__( self, predicate_type = None, value = None, inclusive = True, min_current_count = 0, min_pending_count = 0, max_current_count = None, max_pending_count = None ):
		if isinstance( value, ( list, set ) ):
			value = tuple( value )

		self._predicate_type = predicate_type
		self._value = value

		self._inclusive = inclusive

		self._min_current_count = min_current_count
		self._min_pending_count = min_pending_count
		self._max_current_count = max_current_count
		self._max_pending_count = max_pending_count

	def __eq__( self, other ):
		return self.__hash__() == other.__hash__()

	def __hash__( self ):
		return ( self._predicate_type, self._value, self._inclusive ).__hash__()

	def __ne__( self, other ):
		return self.__hash__() != other.__hash__()

	def __repr__( self ):
		return 'Predicate: ' + str( ( self._predicate_type, self._value, self._inclusive, self.GetCount() ) )

	def _GetSerialisableInfo( self ):
		if self._predicate_type in ( HC.PREDICATE_TYPE_SYSTEM_RATING, HC.PREDICATE_TYPE_SYSTEM_FILE_SERVICE ):
			( operator, value, service_key ) = self._value

			serialisable_value = ( operator, value, service_key.hex() )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIMILAR_TO:
			( hash, max_hamming ) = self._value

			serialisable_value = ( hash.hex(), max_hamming )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS:
			( operator, rule_type, rule, description ) = self._value

			if rule_type == 'url_match':
				serialisable_rule = rule.GetSerialisableTuple()

			else:
				serialisable_rule = rule

			serialisable_value = ( operator, rule_type, serialisable_rule, description )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_HASH:
			( hash, hash_type ) = self._value

			serialisable_value = ( hash.hex(), hash_type )

		elif self._predicate_type == HC.PREDICATE_TYPE_OR_CONTAINER:
			or_predicates = self._value

			serialisable_value = SerialisableList( or_predicates ).GetSerialisableTuple()

		else:
			serialisable_value = self._value

		return ( self._predicate_type, serialisable_value, self._inclusive )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( self._predicate_type, serialisable_value, self._inclusive ) = serialisable_info

		if self._predicate_type in ( HC.PREDICATE_TYPE_SYSTEM_RATING, HC.PREDICATE_TYPE_SYSTEM_FILE_SERVICE ):
			( operator, value, service_key ) = serialisable_value

			self._value = ( operator, value, bytes.fromhex( service_key ) )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIMILAR_TO:
			( serialisable_hash, max_hamming ) = serialisable_value

			self._value = ( bytes.fromhex( serialisable_hash ), max_hamming )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS:
			( operator, rule_type, serialisable_rule, description ) = serialisable_value

			if rule_type == 'url_match':
				rule = CreateFromSerialisableTuple( serialisable_rule )

			else:
				rule = serialisable_rule

			self._value = ( operator, rule_type, rule, description )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_HASH:
			( serialisable_hash, hash_type ) = serialisable_value

			self._value = ( bytes.fromhex( serialisable_hash ), hash_type )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_AGE:
			( operator, age_type, age_value ) = serialisable_value

			self._value = ( operator, age_type, tuple( age_value ) )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS:
			( view_type, viewing_locations, operator, viewing_value ) = serialisable_value

			self._value = ( view_type, tuple( viewing_locations ), operator, viewing_value )

		elif self._predicate_type == HC.PREDICATE_TYPE_OR_CONTAINER:
			serialisable_or_predicates = serialisable_value

			self._value = tuple( CreateFromSerialisableTuple( serialisable_or_predicates ) )

		else:
			self._value = serialisable_value

		if isinstance( self._value, list ):
			self._value = tuple( self._value )

	def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
		if version == 1:
			( predicate_type, serialisable_value, inclusive ) = old_serialisable_info

			if predicate_type == HC.PREDICATE_TYPE_SYSTEM_AGE:
				( operator, years, months, days, hours ) = serialisable_value

				serialisable_value = ( operator, 'delta', ( years, months, days, hours ) )

			new_serialisable_info = ( predicate_type, serialisable_value, inclusive )

			return ( 2, new_serialisable_info )

	def AddCounts( self, predicate ):
		( min_current_count, max_current_count, min_pending_count, max_pending_count ) = predicate.GetAllCounts()

		( self._min_current_count, self._max_current_count ) = MergeCounts( self._min_current_count, self._max_current_count, min_current_count, max_current_count )
		( self._min_pending_count, self._max_pending_count) = MergeCounts( self._min_pending_count, self._max_pending_count, min_pending_count, max_pending_count )

	def GetAllCounts( self ):
		return ( self._min_current_count, self._max_current_count, self._min_pending_count, self._max_pending_count )

	def GetCopy( self ):
		return Predicate( self._predicate_type, self._value, self._inclusive, self._min_current_count, self._min_pending_count, self._max_current_count, self._max_pending_count )

	def GetCountlessCopy( self ):
		return Predicate( self._predicate_type, self._value, self._inclusive )

	def GetCount( self, current_or_pending = None ):
		if current_or_pending is None:
			return self._min_current_count + self._min_pending_count

		elif current_or_pending == HC.CONTENT_STATUS_CURRENT:
			return self._min_current_count

		elif current_or_pending == HC.CONTENT_STATUS_PENDING:
			return self._min_pending_count

	def GetNamespace( self ):
		if self._predicate_type in HC.SYSTEM_PREDICATES:
			return 'system'

		elif self._predicate_type == HC.PREDICATE_TYPE_NAMESPACE:
			namespace = self._value

			return namespace

		elif self._predicate_type in ( HC.PREDICATE_TYPE_PARENT, HC.PREDICATE_TYPE_TAG, HC.PREDICATE_TYPE_WILDCARD ):
			tag_analogue = self._value

			( namespace, subtag ) = SplitTag( tag_analogue )

			return namespace

		else:
			return ''

	def GetInclusive( self ):
		# patch from an upgrade mess-up ~v144
		if not hasattr( self, '_inclusive' ):
			if self._predicate_type not in HC.SYSTEM_PREDICATES:
				( operator, value ) = self._value

				self._value = value

				self._inclusive = operator == '+'

			else:
				self._inclusive = True

		return self._inclusive

	def GetInfo( self ):
		return ( self._predicate_type, self._value, self._inclusive )

	def GetInverseCopy( self ):
		if self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_ARCHIVE:
			return Predicate( HC.PREDICATE_TYPE_SYSTEM_INBOX )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_INBOX:
			return Predicate( HC.PREDICATE_TYPE_SYSTEM_ARCHIVE )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_LOCAL:
			return Predicate( HC.PREDICATE_TYPE_SYSTEM_NOT_LOCAL )

		elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_NOT_LOCAL:
			return Predicate( HC.PREDICATE_TYPE_SYSTEM_LOCAL )

		elif self._predicate_type in ( HC.PREDICATE_TYPE_TAG, HC.PREDICATE_TYPE_NAMESPACE, HC.PREDICATE_TYPE_WILDCARD ):
			return Predicate( self._predicate_type, self._value, not self._inclusive )

		else:
			return None

	def GetTextsAndNamespaces( self, or_under_construction = False ):
		if self._predicate_type == HC.PREDICATE_TYPE_OR_CONTAINER:
			texts_and_namespaces = []

			if or_under_construction:
				texts_and_namespaces.append( ( 'OR: ', 'system' ) )

			for or_predicate in self._value:
				texts_and_namespaces.append( ( or_predicate.ToString(), or_predicate.GetNamespace() ) )

				texts_and_namespaces.append( ( ' OR ', 'system' ) )

			texts_and_namespaces = texts_and_namespaces[ : -1 ]

		else:
			texts_and_namespaces = [ ( self.ToString(), self.GetNamespace() ) ]

		return texts_and_namespaces

	def GetType( self ):
		return self._predicate_type

	def IsInclusive( self ):
		return self._inclusive

	def ToString( self, with_count = True, sibling_service_key = None, render_for_user = False, or_under_construction = False ):
		count_text = ''

		if with_count:
			if self._min_current_count > 0:
				number_text = ToHumanInt( self._min_current_count )

				if self._max_current_count is not None:
					number_text += '-' + ToHumanInt( self._max_current_count )

				count_text += ' (' + number_text + ')'

			if self._min_pending_count > 0:
				number_text = ToHumanInt( self._min_pending_count )

				if self._max_pending_count is not None:
					number_text += '-' + ToHumanInt( self._max_pending_count )

				count_text += ' (+' + number_text + ')'

		if self._predicate_type in HC.SYSTEM_PREDICATES:
			if self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_EVERYTHING:
				base = 'everything'
			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_INBOX:
				base = 'inbox'
			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_ARCHIVE:
				base = 'archive'
			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_UNTAGGED:
				base = 'untagged'
			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_LOCAL:
				base = 'local'
			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_NOT_LOCAL:
				base = 'not local'
			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_DIMENSIONS:
				base = 'dimensions'
			elif self._predicate_type in ( HC.PREDICATE_TYPE_SYSTEM_NUM_TAGS, HC.PREDICATE_TYPE_SYSTEM_WIDTH, HC.PREDICATE_TYPE_SYSTEM_HEIGHT, HC.PREDICATE_TYPE_SYSTEM_NUM_WORDS ):
				if self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_NUM_TAGS:
					base = 'number of tags'
				elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_WIDTH:
					base = 'width'
				elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_HEIGHT:
					base = 'height'
				elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_NUM_WORDS:
					base = 'number of words'

				if self._value is not None:
					( operator, value ) = self._value

					base += ' ' + operator + ' ' + ToHumanInt( value )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_DURATION:
				base = 'duration'

				if self._value is not None:
					( operator, value ) = self._value

					base += ' ' + operator + ' ' + ConvertMillisecondsToPrettyTime( value )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_RATIO:
				base = 'ratio'

				if self._value is not None:
					( operator, ratio_width, ratio_height ) = self._value

					base += ' ' + operator + ' ' + str( ratio_width ) + ':' + str( ratio_height )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIZE:
				base = 'size'

				if self._value is not None:
					( operator, size, unit ) = self._value

					base += ' ' + operator + ' ' + str( size ) + ConvertIntToUnit( unit )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_LIMIT:
				base = 'limit'

				if self._value is not None:
					value = self._value

					base += ' is ' + ToHumanInt( value )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_AGE:
				base = 'time imported'

				if self._value is not None:
					( operator, age_type, age_value ) = self._value

					if age_type == 'delta':
						( years, months, days, hours ) = age_value

						DAY = 86400
						MONTH = DAY * 30
						YEAR = MONTH * 12

						time_delta = 0

						time_delta += hours * 3600
						time_delta += days * DAY
						time_delta += months * MONTH
						time_delta += years * YEAR

						if operator == '<':
							pretty_operator = 'since '

						elif operator == '>':
							pretty_operator = 'before '

						elif operator == '\u2248':
							pretty_operator = 'around '

						base += ': ' + pretty_operator + TimeDeltaToPrettyTimeDelta( time_delta ) + ' ago'

					elif age_type == 'date':
						( year, month, day ) = age_value

						dt = datetime.datetime( year, month, day )

						# make a timestamp (IN GMT SECS SINCE 1970) from the local meaning of 2018/02/01
						timestamp = int( time.mktime( dt.timetuple() ) )

						if operator == '<':
							pretty_operator = 'before '

						elif operator == '>':
							pretty_operator = 'since '

						elif operator == '=':
							pretty_operator = 'on the day of '

						elif operator == '\u2248':
							pretty_operator = 'a month either side of '

						# convert this GMT TIMESTAMP to a pretty local string
						base += ': ' + pretty_operator + ConvertTimestampToPrettyTime( timestamp, include_24h_time = False )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_NUM_PIXELS:
				base = 'num_pixels'

				if self._value is not None:
					( operator, num_pixels, unit ) = self._value

					base += ' ' + operator + ' ' + str( num_pixels ) + ' ' + ConvertIntToPixels( unit )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS:
				base = 'known url'

				if self._value is not None:
					( operator, rule_type, rule, description ) = self._value

					base = description

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_HASH:
				base = 'hash'

				if self._value is not None:
					( hash, hash_type ) = self._value

					base = hash_type + ' hash is ' + hash.hex()

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_MIME:
				base = 'mime'

				if self._value is not None:
					mimes = self._value

					if set( mimes ) == set( HC.SEARCHABLE_MIMES ):
						mime_text = 'anything'

					elif set( mimes ) == set( HC.SEARCHABLE_MIMES ).intersection( set( HC.APPLICATIONS ) ):
						mime_text = 'application'

					elif set( mimes ) == set( HC.SEARCHABLE_MIMES ).intersection( set( HC.AUDIO ) ):
						mime_text = 'audio'

					elif set( mimes ) == set( HC.SEARCHABLE_MIMES ).intersection( set( HC.IMAGES ) ):
						mime_text = 'image'

					elif set( mimes ) == set( HC.SEARCHABLE_MIMES ).intersection( set( HC.VIDEO ) ):
						mime_text = 'video'

					else:
						mime_text = ', '.join( [ HC.mime_string_lookup[ mime ] for mime in mimes ] )

					base += ' is ' + mime_text

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_RATING:
				base = 'rating'

				if self._value is not None:
					( operator, value, service_key ) = self._value

					try:
						service = HG.client_controller.services_manager.GetService( service_key )

						service_type = service.GetServiceType()

						pretty_value = str( value )

						if service_type == HC.LOCAL_RATING_LIKE:
							if value == 0:
								pretty_value = 'dislike'

							elif value == 1:
								pretty_value = 'like'

						elif service_type == HC.LOCAL_RATING_NUMERICAL:
							if isinstance( value, float ):
								allow_zero = service.AllowZero()
								num_stars = service.GetNumStars()

								if allow_zero:
									star_range = num_stars

								else:
									star_range = num_stars - 1

								pretty_x = int( round( value * star_range ) )
								pretty_y = num_stars

								if not allow_zero:
									pretty_x += 1

								pretty_value = ConvertValueRangeToPrettyString( pretty_x, pretty_y )

						base += ' for ' + service.GetName() + ' ' + operator + ' ' + pretty_value

					except DataMissing:
						base = 'system:unknown rating service system predicate'

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIMILAR_TO:
				base = 'similar to'

				if self._value is not None:
					( hash, max_hamming ) = self._value

					base += ' ' + hash.hex() + ' using max hamming of ' + str( max_hamming )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_FILE_SERVICE:
				if self._value is None:
					base = 'file service'

				else:
					( operator, current_or_pending, service_key ) = self._value

					if operator is True:
						base = 'is'
					else:
						base = 'is not'

					if current_or_pending == HC.CONTENT_STATUS_PENDING:
						base += ' pending to '
					else:
						base += ' currently in '

					try:
						service = HG.client_controller.services_manager.GetService( service_key )

						base += service.GetName()

					except DataMissing:
						base = 'unknown file service system predicate'

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER:
				if self._value is None:
					base = 'tag as number'

				else:
					( namespace, operator, num ) = self._value

					if namespace == '':
						n_text = 'tag'

					else:
						n_text = namespace

					if operator == '\u2248':
						o_text = ' about '

					elif operator == '<':
						o_text = ' less than '

					elif operator == '>':
						o_text = ' more than '

					base = n_text + o_text + ToHumanInt( num )

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_DUPLICATE_RELATIONSHIPS:
				base = 'num duplicate relationships'

				if self._value is not None:
					( operator, num_relationships, dupe_type ) = self._value

					if operator == '\u2248':
						o_text = ' about '

					elif operator == '<':
						o_text = ' less than '

					elif operator == '>':
						o_text = ' more than '

					elif operator == '=':
						o_text = ' '

					base += ' - has' + o_text + ToHumanInt( num_relationships ) + ' ' + HC.duplicate_type_string_lookup[ dupe_type ]

			elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS:
				base = 'file viewing statistics'

				if self._value is not None:
					( view_type, viewing_locations, operator, viewing_value ) = self._value

					include_media = 'media' in viewing_locations
					include_previews = 'preview' in viewing_locations

					if include_media and include_previews:
						domain = 'all '

					elif include_media:
						domain = 'media '

					elif include_previews:
						domain = 'preview '

					else:
						domain = 'unknown '

					if view_type == 'views':
						value_string = ToHumanInt( viewing_value )

					elif view_type == 'viewtime':
						value_string = TimeDeltaToPrettyTimeDelta( viewing_value )

					base = domain + view_type + operator + value_string

			base = CombineTag( 'system', base )

			base = RenderTag( base, render_for_user )

			base += count_text

		elif self._predicate_type == HC.PREDICATE_TYPE_TAG:
			tag = self._value

			if not self._inclusive:
				base = '-'
			else:
				base = ''

			base += RenderTag( tag, render_for_user )

			base += count_text

			if sibling_service_key is not None:
				siblings_manager = HG.client_controller.tag_siblings_manager

				sibling = siblings_manager.GetSibling( sibling_service_key, tag )

				if sibling is not None:
					sibling = RenderTag( sibling, render_for_user )

					base += ' (will display as ' + sibling + ')'

		elif self._predicate_type == HC.PREDICATE_TYPE_PARENT:
			base = '	'

			tag = self._value

			base += RenderTag( tag, render_for_user )

			base += count_text

		elif self._predicate_type == HC.PREDICATE_TYPE_NAMESPACE:
			namespace = self._value

			if not self._inclusive:
				base = '-'
			else:
				base = ''

			anything_tag = CombineTag( namespace, '*anything*' )

			anything_tag = RenderTag( anything_tag, render_for_user )

			base += anything_tag

		elif self._predicate_type == HC.PREDICATE_TYPE_WILDCARD:
			wildcard = self._value + ' (wildcard search)'

			if not self._inclusive:
				base = '-'

			else:
				base = ''

			base += wildcard

		elif self._predicate_type == HC.PREDICATE_TYPE_OR_CONTAINER:
			or_predicates = self._value

			base = ''

			if or_under_construction:
				base += 'OR: '

			base += ' OR '.join( ( or_predicate.ToString( render_for_user = render_for_user ) for or_predicate in or_predicates ) )

		return base

	def GetUnnamespacedCopy( self ):
		if self._predicate_type == HC.PREDICATE_TYPE_TAG:
			( namespace, subtag ) = SplitTag( self._value )

			return Predicate( self._predicate_type, subtag, self._inclusive, self._min_current_count, self._min_pending_count, self._max_current_count, self._max_pending_count )

		return self.GetCopy()

	def GetValue( self ):
		return self._value

	def HasNonZeroCount( self ):
		return self._min_current_count > 0 or self._min_pending_count > 0

	def SetInclusive( self, inclusive ):
		self._inclusive = inclusive

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["PREDICATE"] ] = Predicate

SYSTEM_PREDICATE_INBOX = Predicate( HC.PREDICATE_TYPE_SYSTEM_INBOX, None )

SYSTEM_PREDICATE_ARCHIVE = Predicate( HC.PREDICATE_TYPE_SYSTEM_ARCHIVE, None )

SYSTEM_PREDICATE_LOCAL = Predicate( HC.PREDICATE_TYPE_SYSTEM_LOCAL, None )

SYSTEM_PREDICATE_NOT_LOCAL = Predicate( HC.PREDICATE_TYPE_SYSTEM_NOT_LOCAL, None )