code:
from .DB import *

__all__ = [
	"Local"
]

from . import *


code:
import random, threading, time

from lib.Actions.Import.Options import FileImportOptions
from lib.Actions.Import.FileSeeds import FileImportJob

from lib.Hydrus.Data import ContentUpdate, ShowException, ShowText, ToHumanInt
from lib.Hydrus.Exceptions import CancelledException, ServerBusyException
from lib.Hydrus.Paths import CleanUpTempPath
from lib.Hydrus.Paths.Local import GetTempPath
from lib.Hydrus.Threading import IsThreadShuttingDown, JobKey, ShutdownThread

### CONSTANTS ###

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

### FUNCTIONS ###

def DAEMONCheckExportFolders():
	controller = HG.client_controller

	if not controller.options[ 'pause_export_folders_sync' ]:
		HG.export_folders_running = True

		try:
			export_folder_names = controller.Read( 'serialisable_names', HG.SerialisableType["EXPORT_FOLDER"] )

			for name in export_folder_names:
				export_folder = controller.Read( 'serialisable_named', HG.SerialisableType["EXPORT_FOLDER"], name )

				if controller.options[ 'pause_export_folders_sync' ] or IsThreadShuttingDown():
					break

				export_folder.DoWork()

		finally:
			HG.export_folders_running = False

def DAEMONCheckImportFolders():
	controller = HG.client_controller

	if not controller.options[ 'pause_import_folders_sync' ]:
		HG.import_folders_running = True

		try:
			import_folder_names = controller.Read( 'serialisable_names', HG.SerialisableType["IMPORT_FOLDER"] )

			for name in import_folder_names:
				import_folder = controller.Read( 'serialisable_named', HG.SerialisableType["IMPORT_FOLDER"], name )

				if controller.options[ 'pause_import_folders_sync' ] or IsThreadShuttingDown():
					break

				import_folder.DoWork()

		finally:
			HG.import_folders_running = False

def DAEMONDownloadFiles( controller ):
	hashes = controller.Read( 'downloads' )

	num_downloads = len( hashes )

	if num_downloads > 0:
		client_files_manager = controller.client_files_manager

		successful_hashes = set()

		job_key = JobKey()

		job_key.SetVariable( 'popup_text_1', 'initialising downloader' )

		controller.pub( 'message', job_key )

		for hash in hashes:
			job_key.SetVariable( 'popup_text_1', 'downloading ' + ToHumanInt( num_downloads - len( successful_hashes ) ) + ' files from repositories' )

			( media_result, ) = controller.Read( 'media_results', ( hash, ) )

			service_keys = list( media_result.GetLocationsManager().GetCurrent() )

			random.shuffle( service_keys )

			for service_key in service_keys:
				if service_key == CC.LOCAL_FILE_SERVICE_KEY:
					break
				elif service_key == CC.TRASH_SERVICE_KEY:
					continue

				try:
					service = controller.services_manager.GetService( service_key )

				except Exception:
					continue

				if service.GetServiceType() == HC.FILE_REPOSITORY:
					file_repository = service

					if file_repository.IsFunctional():
						try:
							( os_file_handle, temp_path ) = GetTempPath()

							try:
								file_repository.Request( HC.GET, 'file', { 'hash' : hash }, temp_path = temp_path )

								controller.WaitUntilModelFree()

								exclude_deleted = False # this is the important part here
								do_not_check_known_urls_before_importing = False
								do_not_check_hashes_before_importing = False
								allow_decompression_bombs = True
								min_size = None
								max_size = None
								max_gif_size = None
								min_resolution = None
								max_resolution = None
								automatic_archive = False
								associate_source_urls = True

								file_import_options = FileImportOptions()

								file_import_options.SetPreImportOptions( exclude_deleted, do_not_check_known_urls_before_importing, do_not_check_hashes_before_importing, allow_decompression_bombs, min_size, max_size, max_gif_size, min_resolution, max_resolution )
								file_import_options.SetPostImportOptions( automatic_archive, associate_source_urls )

								file_import_job = FileImportJob( temp_path, file_import_options )

								client_files_manager.ImportFile( file_import_job )

								successful_hashes.add( hash )

								break

							finally:
								CleanUpTempPath( os_file_handle, temp_path )

						except ServerBusyException:
							job_key.SetVariable( 'popup_text_1', file_repository.GetName() + ' was busy. waiting 30s before trying again' )

							time.sleep( 30 )

							job_key.Delete()

							controller.pub( 'notify_new_downloads' )

							return

						except Exception as e:
							ShowText( 'Error downloading file!' )
							ShowException( e )

				elif service.GetServiceType() == HC.IPFS:
					multihashes = HG.client_controller.Read( 'service_filenames', service_key, { hash } )

					if len( multihashes ) > 0:
						multihash = multihashes[0]

						# this actually calls to a thread that can launch gui 'select from tree' stuff, so let's just break at this point
						service.ImportFile( multihash )

						break

				if IsThreadShuttingDown():
					return

		if len( successful_hashes ) > 0:
			job_key.SetVariable( 'popup_text_1', ToHumanInt( len( successful_hashes ) ) + ' files downloaded' )

		job_key.Delete()

def DAEMONMaintainTrash( controller ):
	if HC.options[ 'trash_max_size' ] is not None:
		max_size = HC.options[ 'trash_max_size' ] * 1048576

		service_info = controller.Read( 'service_info', CC.TRASH_SERVICE_KEY )

		while service_info[ HC.SERVICE_INFO_TOTAL_SIZE ] > max_size:
			if IsThreadShuttingDown():
				return

			hashes = controller.Read( 'trash_hashes', limit = 10 )

			if len( hashes ) == 0:
				return

			content_update = ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes )

			service_keys_to_content_updates = { CC.TRASH_SERVICE_KEY : [ content_update ] }

			controller.WaitUntilModelFree()

			controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )

			service_info = controller.Read( 'service_info', CC.TRASH_SERVICE_KEY )

			time.sleep( 2 )

	if HC.options[ 'trash_max_age' ] is not None:
		max_age = HC.options[ 'trash_max_age' ] * 3600

		hashes = controller.Read( 'trash_hashes', limit = 10, minimum_age = max_age )

		while len( hashes ) > 0:
			if IsThreadShuttingDown():
				return

			content_update = ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes )

			service_keys_to_content_updates = { CC.TRASH_SERVICE_KEY : [ content_update ] }

			controller.WaitUntilModelFree()

			controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )

			hashes = controller.Read( 'trash_hashes', limit = 10, minimum_age = max_age )

			time.sleep( 2 )

def DAEMONSynchroniseRepositories( controller ):
	if not controller.options[ 'pause_repo_sync' ]:
		services = controller.services_manager.GetServices( HC.REPOSITORIES )

		for service in services:
			if IsThreadShuttingDown():
				return

			if controller.options[ 'pause_repo_sync' ]:
				return

			service.Sync( only_process_when_idle = True )

			if IsThreadShuttingDown():
				return

			time.sleep( 3 )

### CLASSES ###

class SubscriptionJob( object ):
	def __init__( self, controller, name ):
		self._controller = controller
		self._name = name
		self._job_done = threading.Event()

	def _DoWork( self ):
		if HG.subscription_report_mode:
			ShowText( 'Subscription "' + self._name + '" about to start.' )

		subscription = self._controller.Read( 'serialisable_named', HG.SerialisableType["SUBSCRIPTION"], self._name )

		subscription.Sync()

	def IsDone( self ):
		return self._job_done.is_set()

	def Work( self ):
		try:
			self._DoWork()

		finally:
			self._job_done.set()

def DAEMONSynchroniseSubscriptions( controller ):
	def filter_finished_jobs( subs_jobs ):
		done_indices = [ i for ( i, ( thread, job ) ) in enumerate( subs_jobs ) if job.IsDone() ]

		done_indices.reverse()

		for i in done_indices:
			del subs_jobs[ i ]

	def wait_for_free_slot( controller, subs_jobs, max_simultaneous_subscriptions ):
		time.sleep( 0.1 )

		while True:
			p1 = controller.options[ 'pause_subs_sync' ]
			p2 = IsThreadShuttingDown()

			if p1 or p2:
				if HG.subscription_report_mode:
					ShowText( 'Subscriptions cancelling. Global sub pause is ' + str( p1 ) + ' and sub daemon thread shutdown status is ' + str( p2 ) + '.' )

				if p2:
					for ( thread, job ) in subs_jobs:
						ShutdownThread( thread )

				raise CancelledException( 'subs cancelling or thread shutting down' )

			filter_finished_jobs( subs_jobs )

			if len( subs_jobs ) < max_simultaneous_subscriptions:
				return

			time.sleep( 1.0 )

	def wait_for_all_finished( subs_jobs ):
		while True:
			filter_finished_jobs( subs_jobs )

			if len( subs_jobs ) == 0:
				return

			time.sleep( 1.0 )

	if HG.subscription_report_mode:
		ShowText( 'Subscription daemon started a run.' )

	subscription_names = list( controller.Read( 'serialisable_names', HG.SerialisableType["SUBSCRIPTION"] ) )

	if controller.new_options.GetBoolean( 'process_subs_in_random_order' ):
		random.shuffle( subscription_names )

	else:
		subscription_names.sort()

	HG.subscriptions_running = True

	subs_jobs = []

	try:
		for name in subscription_names:
			max_simultaneous_subscriptions = controller.new_options.GetInteger( 'max_simultaneous_subscriptions' )

			try:
				wait_for_free_slot( controller, subs_jobs, max_simultaneous_subscriptions )

			except CancelledException:
				break

			job = SubscriptionJob( controller, name )

			thread = threading.Thread( target = job.Work, name = 'subscription thread' )

			thread.start()

			subs_jobs.append( ( thread, job ) )

		wait_for_all_finished( subs_jobs )

	finally:
		HG.subscriptions_running = False

license: wtfpl


code:
import collections, cProfile, io, locale, os, pstats, psutil, random, re, \
	sqlite3, struct, subprocess, sys, threading, time, traceback, yaml, \
	itertools, wx, wx.lib.colourutils

from lib.Hydrus.Text import DeserialiseNewlinedTexts
from lib.Hydrus.Exceptions import ShutdownException
from lib.Hydrus.Serialisable import SerialisableBase, CreateFromSerialisableTuple
from lib.Hydrus.Threading import JobKey

### CONSTANTS ###

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

### FUNCTIONS ###

def AddPaddingToDimensions( dimensions, padding ):
	( x, y ) = dimensions

	return ( x + padding, y + padding )

def default_dict_list():
	return collections.defaultdict( list )

def default_dict_set():
	return collections.defaultdict( set )

def BuildKeyToListDict( pairs ):
	d = collections.defaultdict( list )

	for ( key, value ) in pairs:
		d[ key ].append( value )

	return d

def BuildKeyToSetDict( pairs ):
	d = collections.defaultdict( set )

	for ( key, value ) in pairs:
		d[ key ].add( value )

	return d

def CalculateScoreFromRating( count, rating ):
	# http://www.evanmiller.org/how-not-to-sort-by-average-rating.html

	positive = count * rating
	negative = count * ( 1.0 - rating )

	# positive + negative = count

	# I think I've parsed this correctly from the website! Not sure though!
	score = ( ( positive + 1.9208 ) / count - 1.96 * ( ( ( positive * negative ) / count + 0.9604 ) ** 0.5 ) / count ) / ( 1 + 3.8416 / count )

	return score

def CleanRunningFile( db_path, instance ):
	path = os.path.join( db_path, instance + '_running' )

	try:
		os.remove( path )

	except Exception:
		pass

def ConvertFloatToPercentage( f ):
	return '{:.1f}%'.format( f * 100 )

def ConvertIntToFirst( n ):
	# straight from stack, wew
	return "%d%s" % (n, "tsnrhtdd"[( n/10%10!=1 )*( n%10<4 )*n%10::4])

def ConvertIntToPixels( i ):
	if i == 1:
		return 'pixels'
	elif i == 1000:
		return 'kilopixels'
	elif i == 1000000:
		return 'megapixels'
	else:
		return 'megapixels'

def ConvertIntToPrettyOrdinalString( num ):
	remainder = num % 10

	if remainder == 1:
		ordinal = 'st'

	elif remainder == 2:
		ordinal = 'nd'

	elif remainder == 3:
		ordinal = 'rd'

	else:
		ordinal = 'th'

	return ToHumanInt( num ) + ordinal

def ConvertIntToUnit( unit ):
	if unit == 1:
		return 'B'
	elif unit == 1024:
		return 'KB'
	elif unit == 1048576:
		return 'MB'
	elif unit == 1073741824:
		return 'GB'

def ConvertMillisecondsToPrettyTime( ms ):
	hours = ms // 3600000

	if hours == 1:
		hours_result = '1 hour'
	else:
		hours_result = str( hours ) + ' hours'

	ms = ms % 3600000

	minutes = ms // 60000

	if minutes == 1:
		minutes_result = '1 minute'
	else:
		minutes_result = str( minutes ) + ' minutes'

	ms = ms % 60000

	seconds = ms // 1000

	if seconds == 1:
		seconds_result = '1 second'
	else:
		seconds_result = str( seconds ) + ' seconds'

	detailed_seconds = ms / 1000

	detailed_seconds_result = '{:.1f} seconds'.format( detailed_seconds )

	ms = ms % 1000

	if hours > 0:
		return hours_result + ' ' + minutes_result

	if minutes > 0:
		return minutes_result + ' ' + seconds_result

	if seconds > 0:
		return detailed_seconds_result

	ms = int( ms )

	if ms == 1:
		milliseconds_result = '1 millisecond'
	else:
		milliseconds_result = '{} milliseconds'.format( ms )

	return milliseconds_result

def ConvertNumericalRatingToPrettyString( lower, upper, rating, rounded_result = False, out_of = True ):
	rating_converted = ( rating * ( upper - lower ) ) + lower

	if rounded_result:
		rating_converted = round( rating_converted )

	s = '{:.2f}'.format( rating_converted )

	if out_of and lower in ( 0, 1 ):
		s += '/{:.2f}'.format( upper )

	return s

def ConvertPixelsToInt( unit ):
	if unit == 'pixels':
		return 1
	elif unit == 'kilopixels':
		return 1000
	elif unit == 'megapixels':
		return 1000000

def ConvertPrettyStringsToUglyNamespaces( pretty_strings ):
	result = { s for s in pretty_strings if s != 'no namespace' }

	if 'no namespace' in pretty_strings:
		result.add( '' )

	return result

def ConvertResolutionToPrettyString( resolution ):
	( width, height ) = resolution

	return ToHumanInt( width ) + 'x' + ToHumanInt( height )

def ConvertStatusToPrefix( status ):
	if status == HC.CONTENT_STATUS_CURRENT:
		return ''
	elif status == HC.CONTENT_STATUS_PENDING:
		return '(+) '
	elif status == HC.CONTENT_STATUS_PETITIONED:
		return '(-) '
	elif status == HC.CONTENT_STATUS_DELETED:
		return '(X) '

def TimeDeltaToPrettyTimeDelta( seconds ):
	if seconds is None:
		return 'per month'

	if seconds == 0:
		return '0 seconds'

	if seconds < 0:
		seconds = abs( seconds )

	if seconds >= 60:
		seconds = int( seconds )

		MINUTE = 60
		HOUR = 60 * MINUTE
		DAY = 24 * HOUR
		MONTH = 30 * DAY
		YEAR = 12 * MONTH

		lines = []

		lines.append( ( 'year', YEAR ) )
		lines.append( ( 'month', MONTH ) )
		lines.append( ( 'day', DAY ) )
		lines.append( ( 'hour', HOUR ) )
		lines.append( ( 'minute', MINUTE ) )
		lines.append( ( 'second', 1 ) )

		result_components = []

		for ( time_string, duration ) in lines:
			time_quantity = seconds // duration

			seconds %= duration

			if time_quantity > 0:
				s = ToHumanInt( time_quantity ) + ' ' + time_string

				if time_quantity > 1:
					s += 's'

				result_components.append( s )

				if len( result_components ) == 2: # we now have 1 month 2 days
					break

			else:
				if len( result_components ) > 0: # something like '1 year' -- in which case we do not care about the days and hours
					break

		result = ' '.join( result_components )

	elif seconds > 1:
		if int( seconds ) == seconds:
			result = ToHumanInt( seconds ) + ' seconds'

		else:
			result = '{:.1f} seconds'.format( seconds )

	elif seconds == 1:
		result = '1 second'

	elif seconds > 0.1:
		result = '{} milliseconds'.format( int( seconds * 1000 ) )

	elif seconds > 0.01:
		result = '{:.1f} milliseconds'.format( int( seconds * 1000 ) )

	elif seconds > 0.001:
		result = '{:.2f} milliseconds'.format( int( seconds * 1000 ) )

	else:
		result = '{} microseconds'.format( int( seconds * 1000000 ) )

	return result

def ConvertTimestampToPrettyExpires( timestamp ):
	if timestamp is None:
		return 'does not expire'

	if timestamp == 0:
		return 'unknown expiration'

	time_delta_string = TimestampToPrettyTimeDelta( timestamp )

	if TimeHasPassed( timestamp ):
		return 'expired ' + time_delta_string

	else:
		return 'expires ' + time_delta_string

def ConvertTimestampToPrettyTime( timestamp, in_gmt = False, include_24h_time = True ):
	if include_24h_time:
		phrase = '%Y/%m/%d %H:%M:%S'

	else:
		phrase = '%Y/%m/%d'

	if in_gmt:
		struct_time = time.gmtime( timestamp )

		phrase = phrase + ' GMT'

	else:
		struct_time = time.localtime( timestamp )

	return time.strftime( phrase, struct_time )

def TimestampToPrettyTimeDelta( timestamp, just_now_string = 'now', just_now_threshold = 3 ):
	if timestamp is None:
		timestamp = 0

	if HG.client_controller.new_options.GetBoolean( 'always_show_iso_time' ):
		return ConvertTimestampToPrettyTime( timestamp )

	time_delta = abs( timestamp - GetNow() )

	if time_delta <= just_now_threshold:
		return just_now_string

	time_delta_string = TimeDeltaToPrettyTimeDelta( time_delta )

	if TimeHasPassed( timestamp ):
		return time_delta_string + ' ago'

	else:
		return 'in ' + time_delta_string

def ConvertUglyNamespaceToPrettyString( namespace ):
	if namespace is None or namespace == '':
		return 'no namespace'

	else:
		return namespace

def ConvertUglyNamespacesToPrettyStrings( namespaces ):
	namespaces = list( namespaces )

	namespaces.sort()

	result = [ ConvertUglyNamespaceToPrettyString( namespace ) for namespace in namespaces ]

	return result

def ConvertUnitToInt( unit ):
	if unit == 'B':
		return 1
	elif unit == 'KB':
		return 1024
	elif unit == 'MB':
		return 1048576
	elif unit == 'GB':
		return 1073741824

def ConvertValueRangeToBytes( value, range ):
	return ToHumanBytes( value ) + '/' + ToHumanBytes( range )

def ConvertValueRangeToPrettyString( value, range ):
	return ToHumanInt( value ) + '/' + ToHumanInt( range )

def ConvertValueRangeToScanbarTimestampsMS( value_ms, range_ms ):
	value_ms = int( round( value_ms ) )

	range_hours = range_ms // 3600000
	value_hours = value_ms // 3600000
	range_minutes = ( range_ms % 3600000 ) // 60000
	value_minutes = ( value_ms % 3600000 ) // 60000
	range_seconds = ( range_ms % 60000 ) // 1000
	value_seconds = ( value_ms % 60000 ) // 1000
	range_ms = range_ms % 1000
	value_ms = value_ms % 1000

	if range_hours > 0:
		# 0:01:23.033/1:12:57.067
		time_phrase = '{}:{:0>2}:{:0>2}.{:0>3}'

		args = ( value_hours, value_minutes, value_seconds, value_ms, range_hours, range_minutes, range_seconds, range_ms )

	elif range_minutes > 0:
		# 01:23.033/12:57.067 or 0:23.033/1:57.067
		if range_minutes > 9:
			time_phrase = '{:0>2}:{:0>2}.{:0>3}'

		else:
			time_phrase = '{:0>1}:{:0>2}.{:0>3}'

		args = ( value_minutes, value_seconds, value_ms, range_minutes, range_seconds, range_ms )

	else:
		# 23.033/57.067 or 3.033/7.067 or 0.033/0.067
		if range_seconds > 9:
			time_phrase = '{:0>2}.{:0>3}'

		else:
			time_phrase = '{:0>1}.{:0>3}'

		args = ( value_seconds, value_ms, range_seconds, range_ms )

	full_phrase = '{}/{}'.format( time_phrase, time_phrase )

	result = full_phrase.format( *args )

	return result

def DebugPrint( debug_info ):
	Print( debug_info )

	sys.stdout.flush()
	sys.stderr.flush()

def DedupeList( xs ):
	xs_seen = set()

	xs_return = []

	for x in xs:
		if x in xs_seen:
			continue

		xs_return.append( x )

		xs_seen.add( x )

	return xs_return

def GenerateKey():
	return os.urandom( HC.HYDRUS_KEY_LENGTH )

def Get64BitHammingDistance( phash1, phash2 ):
	# old way of doing this was:
	# while xor > 0:
	#
	#	distance += 1
	#	xor &= xor - 1
	#

	# convert to unsigned long long, then xor
	# then through the power of stackexchange magic, we get number of bits in record time
	# Here it is: https://stackoverflow.com/questions/9829578/fast-way-of-counting-non-zero-bits-in-positive-integer/9830282#9830282
	n = struct.unpack( '!Q', phash1 )[0] ^ struct.unpack( '!Q', phash2 )[0]

	n = ( n & 0x5555555555555555 ) + ( ( n & 0xAAAAAAAAAAAAAAAA ) >> 1 ) # 10101010, 01010101
	n = ( n & 0x3333333333333333 ) + ( ( n & 0xCCCCCCCCCCCCCCCC ) >> 2 ) # 11001100, 00110011
	n = ( n & 0x0F0F0F0F0F0F0F0F ) + ( ( n & 0xF0F0F0F0F0F0F0F0 ) >> 4 ) # 11110000, 00001111
	n = ( n & 0x00FF00FF00FF00FF ) + ( ( n & 0xFF00FF00FF00FF00 ) >> 8 ) # etc...
	n = ( n & 0x0000FFFF0000FFFF ) + ( ( n & 0xFFFF0000FFFF0000 ) >> 16 )
	n = ( n & 0x00000000FFFFFFFF ) + ( n >> 32 )

	# you technically are going n & 0xFFFFFFFF00000000 at the end, but that's a no-op with the >> 32 afterwards, so can be omitted

	return n

def GetEmptyDataDict():
	data = collections.defaultdict( default_dict_list )

	return data

def GetNow():
	return int( time.time() )

def GetNowFloat():
	return time.time()

def GetNowPrecise():
	if HC.PLATFORM_WINDOWS:
		return time.clock()

	else:
		return time.time()

def GetSiblingProcessPorts( db_path, instance ):
	path = os.path.join( db_path, instance + '_running' )

	if os.path.exists( path ):
		with open( path, 'r', encoding = 'utf-8' ) as f:
			file_text = f.read()

			try:
				( pid, create_time ) = DeserialiseNewlinedTexts( file_text )

				pid = int( pid )
				create_time = float( create_time )

			except ValueError:
				return None

			try:
				if psutil.pid_exists( pid ):
					ports = []

					p = psutil.Process( pid )

					for conn in p.connections():
						if conn.status == 'LISTEN':
							ports.append( int( conn.laddr[1] ) )

					return ports

			except psutil.Error:
				return None

	return None

def GetSubprocessEnv():
	if HC.RUNNING_FROM_FROZEN_BUILD:
		if HG.subprocess_report_mode:
			env = os.environ.copy()

			ShowText( 'Your pyinstaller env is:	{}'.format( env ) )

		# let's make a proper env for subprocess that doesn't have pyinstaller woo woo in it

		env = os.environ.copy()

		changes_made = False

		lp_key = 'LD_LIBRARY_PATH'
		lp_orig_key = lp_key + '_ORIG'

		if lp_orig_key in env:
			env[ lp_key ] = env[ lp_orig_key ]

			changes_made = True

		if ( HC.PLATFORM_LINUX or HC.PLATFORM_OSX ) and 'PATH' in env:
			# fix for pyinstaller, which drops this stuff for some reason and hence breaks ffmpeg
			path = env[ 'PATH' ]

			path_locations = set( path.split( ':' ) )
			desired_path_locations = [ '/usr/bin', '/usr/local/bin' ]

			for desired_path_location in desired_path_locations:
				if desired_path_location not in path_locations:
					path = desired_path_location + ':' + path

					env[ 'PATH' ] = path

					changes_made = True

		if not changes_made:
			env = None

	else:
		env = None

	return env

def GetSubprocessHideTerminalStartupInfo():
	if HC.PLATFORM_WINDOWS:
		# This suppresses the terminal window that tends to pop up when calling ffmpeg or whatever
		startupinfo = subprocess.STARTUPINFO()

		startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW

	else:
		startupinfo = None

	return startupinfo

def GetSubprocessKWArgs( hide_terminal = True, text = False ):
	sbp_kwargs = {}
	sbp_kwargs[ 'env' ] = GetSubprocessEnv()

	if text:
		# probably need to override the stdXXX pipes with i/o encoding wrappers in the case of 3.5 here
		if sys.version_info.minor >= 6:
			sbp_kwargs[ 'encoding' ] = 'utf-8'

		if sys.version_info.minor >= 7:
			sbp_kwargs[ 'text' ] = True

		else:
			sbp_kwargs[ 'universal_newlines' ] = True

	if hide_terminal:
		sbp_kwargs[ 'startupinfo' ] = GetSubprocessHideTerminalStartupInfo()

#	if HG.subprocess_report_mode:
#		message = 'KWargs are: {}'.format( sbp_kwargs )

	return sbp_kwargs

def GetTimeDeltaSinceTime( timestamp ):
	time_since = timestamp - GetNow()

	result = min( time_since, 0 )

	return - result

def GetTimeDeltaUntilTime( timestamp ):
	time_remaining = timestamp - GetNow()

	return max( time_remaining, 0 )

def GetTimeDeltaUntilTimeFloat( timestamp ):
	time_remaining = timestamp - GetNowFloat()

	return max( time_remaining, 0.0 )

def GetTimeDeltaUntilTimePrecise( t ):
	time_remaining = t - GetNowPrecise()

	return max( time_remaining, 0.0 )

def GetTypeName( obj_type ):
	if hasattr( obj_type, '__name__' ):
		return obj_type.__name__

	else:
		return repr( obj_type )

def GenerateHumanTextSortKey():
	"""Solves the 19, 20, 200, 21, 22 issue when sorting 'Page 21.jpg' type strings.
	Breaks the string into groups of text and int (i.e. ( "Page ", 21, ".jpg" ) )."""
	int_convert = lambda t: int( t ) if t.isdecimal() else t

	split_alphanum = lambda t: tuple( ( int_convert( sub_t ) for sub_t in re.split( '([0-9]+)', t.lower() ) ) )

	return split_alphanum

HumanTextSortKey = GenerateHumanTextSortKey()

def HumanTextSort( texts ):
	texts.sort( key = HumanTextSortKey )

def IntelligentMassIntersect( sets_to_reduce ):
	answer = None

	def get_len( item ):
		return len( item )

	for set_to_reduce in sets_to_reduce:
		if len( set_to_reduce ) == 0:
			return set()

		if answer is None:
			answer = set( set_to_reduce )

		else:
			if len( answer ) == 0:
				return set()

			else:
				answer.intersection_update( set_to_reduce )

	if answer is None:
		return set()

	else:
		return answer

def IsAlreadyRunning( db_path, instance ):
	path = os.path.join( db_path, instance + '_running' )

	if os.path.exists( path ):
		with open( path, 'r', encoding = 'utf-8' ) as f:
			file_text = f.read()

			try:
				( pid, create_time ) = DeserialiseNewlinedTexts( file_text )

				pid = int( pid )
				create_time = float( create_time )

			except ValueError:
				return False

			try:
				me = psutil.Process()

				if me.pid == pid and me.create_time() == create_time:
					# this is me! there is no conflict, lol!
					# this happens when a linux process restarts with os.execl(), for instance (unlike Windows, it keeps its pid)
					return False

				if psutil.pid_exists( pid ):
					p = psutil.Process( pid )

					if p.create_time() == create_time and p.is_running():
						return True

			except psutil.Error:
				return False

	return False

def IterateHexPrefixes():
	hex_chars = '0123456789abcdef'

	for ( one, two ) in itertools.product( hex_chars, hex_chars ):
		prefix = one + two

		yield prefix

def LastShutdownWasBad( db_path, instance ):
	path = os.path.join( db_path, instance + '_running' )

	if os.path.exists( path ):
		return True

	else:
		return False

def MassUnion( lists ):
	return { item for item in itertools.chain.from_iterable( lists ) }

def MedianPop( population ):
	# assume it has at least one and comes sorted
	median_index = len( population ) // 2

	row = population.pop( median_index )

	return row

def MergeKeyToListDicts( key_to_list_dicts ):
	result = collections.defaultdict( list )

	for key_to_list_dict in key_to_list_dicts:
		for ( key, value ) in list( key_to_list_dict.items() ):
			result[ key ].extend( value )

	return result

def Print( text ):
	try:
		print( str( text ) )

	except Exception:
		print( repr( text ) )

ShowText = Print

def PrintException( e, do_wait = True ):
	if isinstance( e, ShutdownException ):
		return

	etype = type( e )

	( etype, value, tb ) = sys.exc_info()

	if etype is None:
		etype = type( e )
		value = str( e )

		trace = 'No error trace'

	else:
		trace = ''.join( traceback.format_exception( etype, value, tb ) )

	stack_list = traceback.format_stack()

	stack = ''.join( stack_list )

	message = str( etype.__name__ ) + ': ' + str( value ) + os.linesep + trace + os.linesep + stack

	Print( '' )
	Print( 'Exception:' )

	DebugPrint( message )

	if do_wait:
		time.sleep( 1 )

ShowException = PrintException

def Profile( summary, code, g, l, min_duration_ms = 20 ):
	profile = cProfile.Profile()

	started = GetNowPrecise()

	profile.runctx( code, g, l )

	finished = GetNowPrecise()

	time_took = finished - started
	time_took_ms = int( time_took * 1000.0 )

	if time_took_ms > min_duration_ms:
		output = io.StringIO()

		stats = pstats.Stats( profile, stream = output )

		stats.strip_dirs()

		stats.sort_stats( 'tottime' )

		output.write( 'Stats' )
		output.write( os.linesep * 2 )

		stats.print_stats()

		output.write( 'Callers' )
		output.write( os.linesep * 2 )

		stats.print_callers()

		output.seek( 0 )

		details = output.read()

	else:
		summary += ' - It took ' + TimeDeltaToPrettyTimeDelta( time_took ) + '.'

		details = ''

	HG.controller.PrintProfile( summary, details )

def RandomPop( population ):
	random_index = random.randint( 0, len( population ) - 1 )

	row = population.pop( random_index )

	return row

def RecordRunningStart( db_path, instance ):
	path = os.path.join( db_path, instance + '_running' )

	record_string = ''

	try:
		me = psutil.Process()

		record_string += str( me.pid )
		record_string += os.linesep
		record_string += str( me.create_time() )

	except psutil.Error:
		return

	with open( path, 'w', encoding = 'utf-8' ) as f:
		f.write( record_string )

def RestartProcess():
	time.sleep( 1 ) # time for ports to unmap

	exe = sys.executable
	me = sys.argv[0]

	if HC.RUNNING_FROM_SOURCE:
		# exe is python's exe, me is the script
		args = [ sys.executable ] + sys.argv

	else:
		# we are running a frozen release--both exe and me are the built exe
		# wrap it in quotes because pyinstaller passes it on as raw text, breaking any path with spaces :/
		args = [ '"' + me + '"' ] + sys.argv[1:]

	os.execv( exe, args )

def SplayListForDB( xs ):
	return '(' + ','.join( ( str( x ) for x in xs ) ) + ')'

def SplitIteratorIntoChunks( iterator, n ):
	chunk = []

	for item in iterator:
		chunk.append( item )

		if len( chunk ) == n:
			yield chunk

			chunk = []

	if len( chunk ) > 0:
		yield chunk

def SplitListIntoChunks( xs, n ):
	if isinstance( xs, set ):
		xs = list( xs )

	for i in range( 0, len( xs ), n ):
		yield xs[ i : i + n ]

def SplitMappingListIntoChunks( xs, n ):
	chunk_weight = 0
	chunk = []

	for ( tag_item, hash_items ) in xs:
		for chunk_of_hash_items in SplitListIntoChunks( hash_items, n ):
			chunk.append( ( tag_item, chunk_of_hash_items ) )

			chunk_weight += len( chunk_of_hash_items )

			if chunk_weight > n:
				yield chunk

				chunk_weight = 0
				chunk = []

	if len( chunk ) > 0:
		yield chunk

def TimeHasPassed( timestamp ):
	if timestamp is None:
		return False

	return GetNow() > timestamp

def TimeHasPassedFloat( timestamp ):
	return GetNowFloat() > timestamp

def TimeHasPassedPrecise( precise_timestamp ):
	return GetNowPrecise() > precise_timestamp

def TimeUntil( timestamp ):
	return timestamp - GetNow()

def ToHumanBytes( size ):
	if size is None:
		return 'unknown size'

	if size < 1024:
		return ToHumanInt( size ) + 'B'

	suffixes = ( '', 'K', 'M', 'G', 'T', 'P' )

	suffix_index = 0

	while size >= 1024:
		size = size / 1024

		suffix_index += 1

	suffix = suffixes[ suffix_index ]

	if size < 10.0:
		# 3.1MB
		return '{:.1f}{}B'.format( size, suffix )

	else:
		# 23MB
		return '{:.0f}{}B'.format( size, suffix )

def ToHumanInt( num ):
	text = locale.format_string( '%d', num, grouping = True )

	return text

def WaitForProcessToFinish( p, timeout ):
	started = GetNow()

	while p.poll() is None:
		if TimeHasPassed( started + timeout ):
			p.kill()

			raise Exception( 'Process did not finish within ' + ToHumanInt( timeout ) + ' seconds!' )

		time.sleep( 2 )

def CatchExceptionClient( etype, value, tb ):
	try:
		trace_list = traceback.format_tb( tb )

		trace = ''.join( trace_list )

		pretty_value = str( value )

		if os.linesep in pretty_value:
			( first_line, anything_else ) = pretty_value.split( os.linesep, 1 )

			trace = trace + os.linesep + anything_else

		else:
			first_line = pretty_value

		job_key = JobKey()

		if etype == ShutdownException:
			return

		else:
			try:
				job_key.SetVariable( 'popup_title', str( etype.__name__ ) )
			except Exception:
				job_key.SetVariable( 'popup_title', str( etype ) )

			job_key.SetVariable( 'popup_text_1', first_line )
			job_key.SetVariable( 'popup_traceback', trace )

		text = job_key.ToString()

		Print( 'Uncaught exception:' )

		DebugPrint( text )

		HG.client_controller.pub( 'message', job_key )

	except Exception:
		text = 'Encountered an error I could not parse:'

		text += os.linesep

		text += str( ( etype, value, tb ) )

		try:
			text += traceback.format_exc()
		except Exception:
			pass

		ShowText( text )

	time.sleep( 1 )

def ColourIsBright( colour ):
	( r, g, b, a ) = colour.Get()

	brightness_estimate = ( r + g + b ) // 3

	it_is_bright = brightness_estimate > 127

	return it_is_bright

def ColourIsGreyish( colour ):
	( r, g, b, a ) = colour.Get()

	greyish = r // 16 == g // 16 and g // 16 == b // 16

	return greyish

def ConvertServiceKeysToContentUpdatesToPrettyString( service_keys_to_content_updates ):
	num_files = 0
	actions = set()
	locations = set()

	extra_words = ''

	for ( service_key, content_updates ) in list( service_keys_to_content_updates.items() ):
		if len( content_updates ) > 0:
			name = HG.client_controller.services_manager.GetName( service_key )

			locations.add( name )

		for content_update in content_updates:
			( data_type, action, row ) = content_update.ToTuple()

			if data_type == HC.CONTENT_TYPE_MAPPINGS:
				extra_words = ' tags for'

			actions.add( HC.content_update_string_lookup[ action ] )

			if action in ( HC.CONTENT_UPDATE_ARCHIVE, HC.CONTENT_UPDATE_INBOX ):
				locations = set()

			num_files += len( content_update.GetHashes() )

	s = ''

	if len( locations ) > 0:
		s += ', '.join( locations ) + '->'

	s += ', '.join( actions ) + extra_words + ' ' + ToHumanInt( num_files ) + ' files'

	return s

def ConvertServiceKeysToTagsToServiceKeysToContentUpdates( hashes, service_keys_to_tags ):
	service_keys_to_content_updates = {}

	for ( service_key, tags ) in service_keys_to_tags.items():
		if len( tags ) == 0:
			continue

		if service_key == CC.LOCAL_TAG_SERVICE_KEY:
			action = HC.CONTENT_UPDATE_ADD

		else:
			action = HC.CONTENT_UPDATE_PEND

		content_updates = [ ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, action, ( tag, hashes ) ) for tag in tags ]

		service_keys_to_content_updates[ service_key ] = content_updates

	return service_keys_to_content_updates

def ConvertZoomToPercentage( zoom ):
	zoom_percent = zoom * 100

	pretty_zoom = '{:.2f}%'.format( zoom_percent )

	if pretty_zoom.endswith( '00%' ):
		pretty_zoom = '{:.0f}%'.format( zoom_percent )

	return pretty_zoom

def GetAlphaOfColour( colour, alpha ):
	( r, g, b, a ) = colour.Get()

	return wx.Colour( r, g, b, alpha )

def GetDifferentLighterDarkerColour( colour, intensity = 3 ):
	( r, g, b, a ) = colour.Get()

	if ColourIsGreyish( colour ):
		if ColourIsBright( colour ):
			colour = wx.Colour( int( g * ( 1 - 0.05 * intensity ) ), b, r )

		else:
			colour = wx.Colour( int( g * ( 1 + 0.05 * intensity ) / 2 ), b, r )

	else:
		colour = wx.Colour( g, b, r )

	return GetLighterDarkerColour( colour, intensity )

def GetLighterDarkerColour( colour, intensity = 3 ):
	if intensity is None or intensity == 0:
		return colour

	if ColourIsBright( colour ):
		return wx.lib.colourutils.AdjustColour( colour, -5 * intensity )

	else:
		( r, g, b, a ) = colour.Get()
		( r, g, b ) = [ max( value, 32 ) for value in ( r, g, b ) ]
		colour = wx.Colour( r, g, b )

		return wx.lib.colourutils.AdjustColour( colour, 5 * intensity )

def GetMediasTagCount( pool, tag_service_key = CC.COMBINED_TAG_SERVICE_KEY, collapse_siblings = False ):
	siblings_manager = HG.client_controller.tag_siblings_manager

	tags_managers = []

	for media in pool:
		if media.IsCollection():
			tags_managers.extend( media.GetSingletonsTagsManagers() )

		else:
			tags_managers.append( media.GetTagsManager() )

	current_tags_to_count = collections.Counter()
	deleted_tags_to_count = collections.Counter()
	pending_tags_to_count = collections.Counter()
	petitioned_tags_to_count = collections.Counter()

	for tags_manager in tags_managers:
		statuses_to_tags = tags_manager.GetStatusesToTags( tag_service_key )

		# combined is already collapsed
		if tag_service_key != CC.COMBINED_TAG_SERVICE_KEY and collapse_siblings:
			statuses_to_tags = siblings_manager.CollapseStatusesToTags( tag_service_key, statuses_to_tags )

		current_tags_to_count.update( statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ] )
		deleted_tags_to_count.update( statuses_to_tags[ HC.CONTENT_STATUS_DELETED ] )
		pending_tags_to_count.update( statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] )
		petitioned_tags_to_count.update( statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ] )

	return ( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count )

def GetSortTypeChoices():
	sort_choices = list( CC.SORT_CHOICES )

	for ( namespaces_text, namespaces_list ) in HC.options[ 'sort_by' ]:
		sort_choices.append( ( namespaces_text, tuple( namespaces_list ) ) )

	service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )

	for service_key in service_keys:
		sort_choices.append( ( 'rating', service_key ) )

	return sort_choices

def MergeCounts( min_a, max_a, min_b, max_b ):
	# 100-None and 100-None returns 100-200
	# 1-None and 4-5 returns 5-6
	# 1-2, and 5-7 returns 6, 9

	if min_a == 0:
		( min_answer, max_answer ) = ( min_b, max_b )

	elif min_b == 0:
		( min_answer, max_answer ) = ( min_a, max_a )

	else:
		if max_a is None:
			max_a = min_a

		if max_b is None:
			max_b = min_b

		min_answer = max( min_a, min_b )
		max_answer = max_a + max_b

	return ( min_answer, max_answer )

def MergePredicates( predicates, add_namespaceless = False ):
	master_predicate_dict = {}

	for predicate in predicates:
		# this works because predicate.__hash__ exists
		if predicate in master_predicate_dict:
			master_predicate_dict[ predicate ].AddCounts( predicate )

		else:
			master_predicate_dict[ predicate ] = predicate

	if add_namespaceless:
		# we want to include the count for namespaced tags in the namespaceless version when:
		# there exists more than one instance of the subtag with different namespaces, including '', that has nonzero count
		unnamespaced_predicate_dict = {}
		subtag_nonzero_instance_counter = collections.Counter()

		for predicate in list( master_predicate_dict.values() ):
			if predicate.HasNonZeroCount():
				unnamespaced_predicate = predicate.GetUnnamespacedCopy()

				subtag_nonzero_instance_counter[ unnamespaced_predicate ] += 1

				if unnamespaced_predicate in unnamespaced_predicate_dict:
					unnamespaced_predicate_dict[ unnamespaced_predicate ].AddCounts( unnamespaced_predicate )

				else:
					unnamespaced_predicate_dict[ unnamespaced_predicate ] = unnamespaced_predicate

		for ( unnamespaced_predicate, count ) in list( subtag_nonzero_instance_counter.items() ):
			# if there were indeed several instances of this subtag, overwrte the master dict's instance with our new count total
			if count > 1:
				master_predicate_dict[ unnamespaced_predicate ] = unnamespaced_predicate_dict[ unnamespaced_predicate ]

	return list( master_predicate_dict.values() )

def OrdIsSensibleASCII( o ):
	return 32 <= o and o <= 127

def OrdIsAlphaLower( o ):
	return 97 <= o and o <= 122

def OrdIsAlphaUpper( o ):
	return 65 <= o and o <= 90

def OrdIsAlpha( o ):
	return OrdIsAlphaLower( o ) or OrdIsAlphaUpper( o )

def OrdIsNumber( o ):
	return 48 <= o and o <= 57

def ReportShutdownException():
	text = 'A serious error occurred while trying to exit the program. Its traceback may be shown next. It should have also been written to client.log. You may need to quit the program from task manager.'

	DebugPrint( text )

	DebugPrint( traceback.format_exc() )

	wx.SafeShowMessage( 'shutdown error', text )
	wx.SafeShowMessage( 'shutdown error', traceback.format_exc() )

def ShowExceptionClient( e, do_wait = True ):
	( etype, value, tb ) = sys.exc_info()

	if etype is None:
		etype = type( e )
		value = str( e )

		trace = 'No error trace--here is the stack:' + os.linesep + ''.join( traceback.format_stack() )

	else:
		trace = ''.join( traceback.format_exception( etype, value, tb ) )

	pretty_value = str( value )

	if os.linesep in pretty_value:
		( first_line, anything_else ) = pretty_value.split( os.linesep, 1 )

		trace = trace + os.linesep + anything_else

	else:
		first_line = pretty_value

	job_key = JobKey()

	if isinstance( e, ShutdownException ):
		return

	else:
		if hasattr( etype, '__name__' ):
			title = str( etype.__name__ )

		else:
			title = str( etype )

		job_key.SetVariable( 'popup_title', title )

		job_key.SetVariable( 'popup_text_1', first_line )
		job_key.SetVariable( 'popup_traceback', trace )

	text = job_key.ToString()

	Print( 'Exception:' )

	DebugPrint( text )

	HG.client_controller.pub( 'message', job_key )

	if do_wait:
		time.sleep( 1 )

def ShowTextClient( text ):
	job_key = JobKey()

	job_key.SetVariable( 'popup_text_1', str( text ) )

	text = job_key.ToString()

	Print( text )

	HG.client_controller.pub( 'message', job_key )

### CLASSES ###

class HydrusYAMLBase( yaml.YAMLObject ):
	yaml_loader = yaml.SafeLoader
	yaml_dumper = yaml.SafeDumper

class AccountIdentifier( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["ACCOUNT_IDENTIFIER"]
	SERIALISABLE_NAME = 'Account Identifier'
	SERIALISABLE_VERSION = 1

	TYPE_ACCOUNT_KEY = 1
	TYPE_CONTENT = 2

	def __init__( self, account_key = None, content = None ):
		HydrusYAMLBase.__init__( self )

		if account_key is not None:
			self._type = self.TYPE_ACCOUNT_KEY
			self._data = account_key

		elif content is not None:
			self._type = self.TYPE_CONTENT
			self._data = content

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

	def __hash__( self ):
		return ( self._type, self._data ).__hash__()

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

	def __repr__( self ):
		return 'Account Identifier:	' + str( ( self._type, self._data ) )

	def _GetSerialisableInfo( self ):
		if self._type == self.TYPE_ACCOUNT_KEY:
			serialisable_data = self._hex()

		elif self._type == self.TYPE_CONTENT:
			serialisable_data = self._GetSerialisableTuple()

		return ( self._type, serialisable_data )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( self._type, serialisable_data ) = serialisable_info

		if self._type == self.TYPE_ACCOUNT_KEY:
			self._data = bytes.fromhex( serialisable_data )

		elif self._type == self.TYPE_CONTENT:
			self._data = CreateFromSerialisableTuple( serialisable_data )

	def GetData( self ):
		return self._data

	def HasAccountKey( self ):
		return self._type == self.TYPE_ACCOUNT_KEY

	def HasContent( self ):
		return self._type == self.TYPE_CONTENT

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["ACCOUNT_IDENTIFIER"] ] = AccountIdentifier

class AccountType( HydrusYAMLBase ):
	yaml_tag = '!AccountType'

	def __init__( self, title, permissions, max_monthly_data ):
		HydrusYAMLBase.__init__( self )

		self._title = title
		self._permissions = permissions
		self._max_monthly_data = max_monthly_data

	def __repr__( self ):
		return self.ConvertToString()

	def GetPermissions( self ):
		return self._permissions

	def GetTitle( self ):
		return self._title

	def GetMaxBytes( self ):
		( max_num_bytes, max_num_requests ) = self._max_monthly_data

		return max_num_bytes

	def GetMaxRequests( self ):
		( max_num_bytes, max_num_requests ) = self._max_monthly_data

		return max_num_requests

	def GetMaxBytesString( self ):
		( max_num_bytes, max_num_requests ) = self._max_monthly_data

		if max_num_bytes is None:
			max_num_bytes_string = 'No limit'
		else:
			max_num_bytes_string = ToHumanBytes( max_num_bytes )

		return max_num_bytes_string

	def GetMaxRequestsString( self ):
		( max_num_bytes, max_num_requests ) = self._max_monthly_data

		if max_num_requests is None:
			max_num_requests_string = 'No limit'
		else:
			max_num_requests_string = ToHumanInt( max_num_requests )

		return max_num_requests_string

	def ConvertToString( self ):
		result_string = self._title + ' with '

		if self._permissions == [ HC.UNKNOWN_PERMISSION ]:
			result_string += 'no permissions'
		else:
			result_string += ', '.join( [ HC.permissions_string_lookup[ permission ] for permission in self._permissions ] ) + ' permissions'

		return result_string

	def IsUnknownAccountType( self ):
		return self._permissions == [ HC.UNKNOWN_PERMISSION ]

	def HasPermission( self, permission ):
		return permission in self._permissions

sqlite3.register_adapter( AccountType, yaml.safe_dump )

class BigJobPauser( object ):
	def __init__( self, period = 10, wait_time = 0.1 ):
		self._period = period
		self._wait_time = wait_time

		self._next_pause = GetNow() + self._period

	def Pause( self ):
		if TimeHasPassed( self._next_pause ):
			time.sleep( self._wait_time )

			self._next_pause = GetNow() + self._period

class Call( object ):
	def __init__( self, func, *args, **kwargs ):
		self._func = func
		self._args = args
		self._kwargs = kwargs

	def __call__( self ):
		self._func( *self._args, **self._kwargs )

	def __repr__( self ):
		return 'Call: ' + repr( ( self._func, self._args, self._kwargs ) )

class ContentUpdate( object ):
	def __init__( self, data_type, action, row, reason = None ):
		self._data_type = data_type
		self._action = action
		self._row = row
		self._reason = reason

	def __eq__( self, other ):
		return hash( self ) == hash( other )

	def __ne__( self, other ):
		return not self.__eq__( other )

	def __hash__( self ):
		return hash( ( self._data_type, self._action, repr( self._row ) ) )

	def __repr__( self ):
		return 'Content Update: ' + str( ( self._data_type, self._action, self._row, self._reason ) )

	def GetAction( self ):
		return self._action

	def GetDataType( self ):
		return self._data_type

	def GetHashes( self ):
		if self._data_type == HC.CONTENT_TYPE_FILES:
			if self._action == HC.CONTENT_UPDATE_ADVANCED:
				hashes = set()

			elif self._action == HC.CONTENT_UPDATE_ADD:
				( file_info_manager, timestamp ) = self._row

				hashes = { file_info_manager.hash }

			else:
				hashes = self._row

		elif self._data_type == HC.CONTENT_TYPE_DIRECTORIES:
			hashes = set()

		elif self._data_type == HC.CONTENT_TYPE_URLS:
			( urls, hashes ) = self._row

		elif self._data_type == HC.CONTENT_TYPE_MAPPINGS:
			if self._action == HC.CONTENT_UPDATE_ADVANCED:
				hashes = set()

			else:
				( tag, hashes ) = self._row

		elif self._data_type in ( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_TYPE_TAG_SIBLINGS ):
			hashes = set()

		elif self._data_type == HC.CONTENT_TYPE_RATINGS:
			if self._action == HC.CONTENT_UPDATE_ADD:
				( rating, hashes ) = self._row

		elif self._data_type == HC.CONTENT_TYPE_NOTES:
			if self._action == HC.CONTENT_UPDATE_SET:
				( notes, hash ) = self._row

				hashes = { hash }

		elif self._data_type == HC.CONTENT_TYPE_FILE_VIEWING_STATS:
			( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = self._row

			hashes = { hash }

		if not isinstance( hashes, set ):
			hashes = set( hashes )

		return hashes

	def GetReason( self ):
		if self._reason is None:
			return 'No reason given.'

		else:
			return self._reason

	def GetWeight( self ):
		return len( self.GetHashes() )

	def IsInboxRelated( self ):
		return self._action in ( HC.CONTENT_UPDATE_ARCHIVE, HC.CONTENT_UPDATE_INBOX )

	def ToTuple( self ):
		return ( self._data_type, self._action, self._row )

class JobDatabase( object ):
	def __init__( self, job_type, synchronous, action, *args, **kwargs ):
		self._type = job_type
		self._synchronous = synchronous
		self._action = action
		self._args = args
		self._kwargs = kwargs

		self._result_ready = threading.Event()

	def GetCallableTuple( self ):
		return ( self._action, self._args, self._kwargs )

	def GetResult( self ):
		time.sleep( 0.00001 ) # this one neat trick can save hassle on superquick jobs as event.wait can be laggy

		while True:
			if self._result_ready.wait( 2 ) is True:
				break

			elif HG.model_shutdown:
				raise ShutdownException( 'Application quit before db could serve result!' )

		if isinstance( self._result, Exception ):
			e = self._result

			raise e

		else:
			return self._result

	def GetType( self ):
		return self._type

	def IsSynchronous( self ):
		return self._synchronous

	def PutResult( self, result ):
		self._result = result

		self._result_ready.set()

	def ToString( self ):
		return self._type + ' ' + self._action

class ServiceUpdate( object ):
	def __init__( self, action, row = None ):
		self._action = action
		self._row = row

	def ToTuple( self ):
		return ( self._action, self._row )

### CLIENT CLASSES ###

class ApplicationCommand( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["APPLICATION_COMMAND"]
	SERIALISABLE_NAME = 'Application Command'
	SERIALISABLE_VERSION = 1

	def __init__( self, command_type = None, data = None ):
		if command_type is None:
			command_type = CC.APPLICATION_COMMAND_TYPE_SIMPLE

		if data is None:
			data = 'archive_file'

		SerialisableBase.__init__( self )

		self._command_type = command_type
		self._data = data

	def __repr__( self ):
		return self.ToString()

	def _GetSerialisableInfo( self ):
		if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
			serialisable_data = self._data

		elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
			( service_key, content_type, action, value ) = self._data

			serialisable_data = ( service_key.hex(), content_type, action, value )

		return ( self._command_type, serialisable_data )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( self._command_type, serialisable_data ) = serialisable_info

		if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
			self._data = serialisable_data

		elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
			( serialisable_service_key, content_type, action, value ) = serialisable_data

			self._data = ( bytes.fromhex( serialisable_service_key ), content_type, action, value )

	def GetCommandType( self ):
		return self._command_type

	def GetData( self ):
		return self._data

	def ToString( self ):
		if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
			return self._data

		elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
			( service_key, content_type, action, value ) = self._data

			components = []

			components.append( HC.content_update_string_lookup[ action ] )
			components.append( HC.content_type_string_lookup[ content_type ] )
			components.append( '"' + str( value ) + '"' )
			components.append( 'for' )

			services_manager = HG.client_controller.services_manager

			if services_manager.ServiceExists( service_key ):
				service = services_manager.GetService( service_key )

				components.append( service.GetName() )

			else:
				components.append( 'unknown service!' )

			return ' '.join( components )

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["APPLICATION_COMMAND"] ] = ApplicationCommand

class Booru( HydrusYAMLBase ):
	yaml_tag = '!Booru'

	def __init__( self, name, search_url, search_separator, advance_by_page_num, thumb_classname, image_id, image_data, tag_classnames_to_namespaces ):
		self._name = name
		self._search_url = search_url
		self._search_separator = search_separator
		self._advance_by_page_num = advance_by_page_num
		self._thumb_classname = thumb_classname
		self._image_id = image_id
		self._image_data = image_data
		self._tag_classnames_to_namespaces = tag_classnames_to_namespaces

	def GetData( self ):
		return ( self._search_url, self._search_separator, self._advance_by_page_num, self._thumb_classname, self._image_id, self._image_data, self._tag_classnames_to_namespaces )

	def GetGalleryParsingInfo( self ):
		return ( self._search_url, self._advance_by_page_num, self._search_separator, self._thumb_classname )

	def GetName( self ):
		return self._name

	def GetNamespaces( self ):
		return list( self._tag_classnames_to_namespaces.values() )

sqlite3.register_adapter( Booru, yaml.safe_dump )

class Credentials( HydrusYAMLBase ):
	yaml_tag = '!Credentials'

	def __init__( self, host, port, access_key = None ):
		HydrusYAMLBase.__init__( self )

		if host == 'localhost':
			host = '127.0.0.1'

		self._host = host
		self._port = port
		self._access_key = access_key

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

	def __hash__( self ):
		return ( self._host, self._port, self._access_key ).__hash__()

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

	def __repr__( self ):
		return 'Credentials: ' + str( ( self._host, self._port, self._access_key.hex() ) )

	def GetAccessKey( self ):
		return self._access_key

	def GetAddress( self ):
		return ( self._host, self._port )

	def GetConnectionString( self ):
		connection_string = ''

		if self.HasAccessKey():
			connection_string += self._access_key.hex() + '@'

		connection_string += self._host + ':' + str( self._port )

		return connection_string

	def HasAccessKey( self ):
		return self._access_key is not None and self._access_key != ''

	def SetAccessKey( self, access_key ):
		self._access_key = access_key

class Imageboard( HydrusYAMLBase ):
	yaml_tag = '!Imageboard'

	def __init__( self, name, post_url, flood_time, form_fields, restrictions ):
		self._name = name
		self._post_url = post_url
		self._flood_time = flood_time
		self._form_fields = form_fields
		self._restrictions = restrictions

	def IsOKToPost( self, media_result ):
		# deleted old code due to deprecation

		return True

	def GetBoardInfo( self ):
		return ( self._post_url, self._flood_time, self._form_fields, self._restrictions )

	def GetName( self ):
		return self._name

sqlite3.register_adapter( Imageboard, yaml.safe_dump )


code:
import os, re, wx

from lib.Actions.Import.Options import CheckerOptions

from lib.GUI.Shortcuts import Shortcuts, Shortcut

from lib.Handling.Parsing import PageParser, SimpleDownloaderParsingFormula

from lib.Hydrus.Data import GetNow
from lib.Hydrus.Data.Local import Imageboard, ApplicationCommand
from lib.Hydrus.Serialisable import CreateFromNetworkBytes, SerialisableList
from lib.Hydrus.Serialisable.Local import LoadFromPng

from lib.Networking.Bandwidth import BandwidthRules
from lib.Networking.Contexts import NetworkContext
from lib.Networking.Domain import GalleryURLGenerator, NestedGalleryURLGenerator, \
	URLMatch
from lib.Networking.Login import LoginScriptDomain

### CONSTANTS ###

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

from lib.Networking.Contexts import GLOBAL_NETWORK_CONTEXT
from lib.Networking.Login import VALID_UNKNOWN, VALID_APPROVED

### FUNCTIONS ###

def GetClientDefaultOptions():
	options = {}

	options[ 'play_dumper_noises' ] = True
	options[ 'default_collect' ] = None
	options[ 'export_path' ] = None
	options[ 'hpos' ] = 400
	options[ 'vpos' ] = 700
	options[ 'thumbnail_cache_size' ] = 25 * 1048576
	options[ 'preview_cache_size' ] = 15 * 1048576
	options[ 'fullscreen_cache_size' ] = 150 * 1048576
	options[ 'thumbnail_dimensions' ] = [ 150, 125 ]
	options[ 'password' ] = None
	options[ 'num_autocomplete_chars' ] = 2
	options[ 'default_gui_session' ] = 'last session'
	options[ 'fetch_ac_results_automatically' ] = True
	options[ 'ac_timings' ] = ( 4, 300, 100 )
	options[ 'idle_period' ] = 60 * 30
	options[ 'idle_mouse_period' ] = 60 * 10
	options[ 'idle_cpu_max' ] = 50
	options[ 'idle_normal' ] = True
	options[ 'idle_shutdown' ] = CC.IDLE_ON_SHUTDOWN_ASK_FIRST
	options[ 'idle_shutdown_max_minutes' ] = 5
	options[ 'maintenance_delete_orphans_period' ] = 86400 * 3
	options[ 'trash_max_age' ] = 72
	options[ 'trash_max_size' ] = 2048
	options[ 'remove_trashed_files' ] = False
	options[ 'remove_filtered_files' ] = False
	options[ 'external_host' ] = None
	options[ 'gallery_file_limit' ] = 2000
	options[ 'always_embed_autocompletes' ] = HC.PLATFORM_LINUX or HC.PLATFORM_OSX
	options[ 'confirm_trash' ] = True
	options[ 'confirm_archive' ] = True
	options[ 'delete_to_recycle_bin' ] = True
	options[ 'animation_start_position' ] = 0.0
	options[ 'hide_preview' ] = False

	regex_favourites = []

	regex_favourites.append( ( r'[1-9]+\d*(?=.{4}$)', '\u2026' + r'0074.jpg -> 74' ) )
	regex_favourites.append( ( r'[^' + re.escape( os.path.sep ) + r']+(?=\s-)', r'E:\my collection\author name - v4c1p0074.jpg -> author name' ) )

	options[ 'regex_favourites' ] = regex_favourites

	system_predicates = {}

	system_predicates[ 'age' ] = ( '<', 'delta', ( 0, 0, 7, 0 ) )
	system_predicates[ 'duration' ] = ( '>', 0 )
	system_predicates[ 'height' ] = ( '=', 1080 )
	system_predicates[ 'limit' ] = 600
	system_predicates[ 'mime' ] = HC.IMAGES
	system_predicates[ 'num_tags' ] = ( '<', 4 )
	system_predicates[ 'ratio' ] = ( '=', 16, 9 )
	system_predicates[ 'size' ] = ( '<', 200, 1024 )
	system_predicates[ 'width' ] = ( '=', 1920 )
	system_predicates[ 'num_words' ] = ( '<', 30000 )
	system_predicates[ 'num_pixels' ] = ( '\u2248', 2, 1000000 )
	system_predicates[ 'hamming_distance' ] = 5

	options[ 'file_system_predicates' ] = system_predicates

	default_namespace_colours = {}

	default_namespace_colours[ 'system' ] = ( 153, 101, 21 )
	default_namespace_colours[ 'meta' ] = ( 0, 0, 0 )
	default_namespace_colours[ 'creator' ] = ( 170, 0, 0 )
	default_namespace_colours[ 'studio' ] = ( 128, 0, 0 )
	default_namespace_colours[ 'character' ] = ( 0, 170, 0 )
	default_namespace_colours[ 'person' ] = ( 0, 128, 0 )
	default_namespace_colours[ 'series' ] = ( 170, 0, 170 )
	default_namespace_colours[ None ] = ( 114, 160, 193 )
	default_namespace_colours[ '' ] = ( 0, 111, 250 )

	options[ 'namespace_colours' ] = default_namespace_colours

	default_sort_by_choices = []

	default_sort_by_choices.append( ( 'namespaces', [ 'series', 'creator', 'title', 'volume', 'chapter', 'page' ] ) )
	default_sort_by_choices.append( ( 'namespaces', [ 'creator', 'series', 'title', 'volume', 'chapter', 'page' ] ) )

	options[ 'sort_by' ] = default_sort_by_choices
	options[ 'show_all_tags_in_autocomplete' ] = True

	options[ 'proxy' ] = None

	options[ 'confirm_client_exit' ] = False

	options[ 'default_tag_repository' ] = CC.LOCAL_TAG_SERVICE_KEY
	options[ 'default_tag_sort' ] = CC.SORT_BY_LEXICOGRAPHIC_ASC

	options[ 'pause_export_folders_sync' ] = False
	options[ 'pause_import_folders_sync' ] = False
	options[ 'pause_repo_sync' ] = False
	options[ 'pause_subs_sync' ] = False

	options[ 'rating_dialog_position' ] = ( False, None )

	return options

def GetDefaultCheckerOptions( name ):
	if name == 'thread':
		return CheckerOptions( intended_files_per_check = 4, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 3 * 86400 ) )

	elif name == 'slow thread':
		return CheckerOptions( intended_files_per_check = 1, never_faster_than = 4 * 3600, never_slower_than = 7 * 86400, death_file_velocity = ( 1, 30 * 86400 ) )

	elif name == 'artist subscription':
		return CheckerOptions( intended_files_per_check = 4, never_faster_than = 86400, never_slower_than = 90 * 86400, death_file_velocity = ( 1, 180 * 86400 ) )

	elif name == 'fast tag subscription':
		return CheckerOptions( intended_files_per_check = 10, never_faster_than = 43200, never_slower_than = 30 * 86400, death_file_velocity = ( 1, 90 * 86400 ) )

	elif name == 'slow tag subscription':
		return CheckerOptions( intended_files_per_check = 1, never_faster_than = 7 * 86400, never_slower_than = 180 * 86400, death_file_velocity = ( 1, 360 * 86400 ) )

def GetDefaultHentaiFoundryInfo():
	info = {}

	info[ 'rating_nudity' ] = '3'
	info[ 'rating_violence' ] = '3'
	info[ 'rating_profanity' ] = '3'
	info[ 'rating_racism' ] = '3'
	info[ 'rating_sex' ] = '3'
	info[ 'rating_spoilers' ] = '3'

	info[ 'rating_yaoi' ] = '1'
	info[ 'rating_yuri' ] = '1'
	info[ 'rating_teen' ] = '1'
	info[ 'rating_guro' ] = '1'
	info[ 'rating_furry' ] = '1'
	info[ 'rating_beast' ] = '1'
	info[ 'rating_male' ] = '1'
	info[ 'rating_female' ] = '1'
	info[ 'rating_futa' ] = '1'
	info[ 'rating_other' ] = '1'
	info[ 'rating_scat' ] = '1'
	info[ 'rating_incest' ] = '1'
	info[ 'rating_rape' ] = '1'

	info[ 'filter_media' ] = 'A'
	info[ 'filter_order' ] = 'date_new'
	info[ 'filter_type' ] = '0'

	info[ 'yt0' ] = 'Apply' # the submit button wew lad

	return info

def GetDefaultGUGs():
	dir_path = os.path.join( HC.STATIC_DIR, 'default', 'gugs' )

	return GetDefaultObjectsFromPNGs( dir_path, ( GalleryURLGenerator, NestedGalleryURLGenerator ) )

def GetDefaultNGUGs():
	gugs = [ gug for gug in GetDefaultGUGs() if isinstance( gug, NestedGalleryURLGenerator ) ]

	return gugs

def GetDefaultSingleGUGs():
	gugs = [ gug for gug in GetDefaultGUGs() if isinstance( gug, GalleryURLGenerator ) ]

	return gugs

def GetDefaultImageboards():
	imageboards = []

	fourchan_common_form_fields = []

	fourchan_common_form_fields.append( ( 'resto', CC.FIELD_THREAD_ID, 'thread_id', True ) )
	fourchan_common_form_fields.append( ( 'email', CC.FIELD_TEXT, '', True ) )
	fourchan_common_form_fields.append( ( 'pwd', CC.FIELD_PASSWORD, '', True ) )
	fourchan_common_form_fields.append( ( 'recaptcha_response_field', CC.FIELD_VERIFICATION_RECAPTCHA, '6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc', True ) )
	fourchan_common_form_fields.append( ( 'com', CC.FIELD_COMMENT, '', True ) )
	fourchan_common_form_fields.append( ( 'upfile', CC.FIELD_FILE, '', True ) )
	fourchan_common_form_fields.append( ( 'mode', CC.FIELD_TEXT, 'regist', False ) )

	fourchan_typical_form_fields = list( fourchan_common_form_fields )

	fourchan_typical_form_fields.insert( 1, ( 'name', CC.FIELD_TEXT, '', True ) )
	fourchan_typical_form_fields.insert( 3, ( 'sub', CC.FIELD_TEXT, '', True ) )

	fourchan_anon_form_fields = list( fourchan_common_form_fields )

	fourchan_anon_form_fields.insert( 1, ( 'name', CC.FIELD_TEXT, '', False ) )
	fourchan_anon_form_fields.insert( 3, ( 'sub', CC.FIELD_TEXT, '', False ) )

	fourchan_spoiler_form_fields = list( fourchan_typical_form_fields )

	fourchan_spoiler_form_fields.append( ( 'spoiler/on', CC.FIELD_CHECKBOX, 'False', True ) )

	GJP = [ HC.IMAGE_GIF, HC.IMAGE_PNG, HC.IMAGE_JPEG ]

	fourchan_typical_restrictions = { CC.RESTRICTION_MAX_FILE_SIZE : 3145728, CC.RESTRICTION_ALLOWED_MIMES : GJP }

	fourchan_imageboards = []

	fourchan_imageboards.append( Imageboard( '/3/', 'https://sys.4chan.org/3/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/a/', 'https://sys.4chan.org/a/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/adv/', 'https://sys.4chan.org/adv/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/an/', 'https://sys.4chan.org/an/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/asp/', 'https://sys.4chan.org/asp/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/b/', 'https://sys.4chan.org/b/post', 75, fourchan_anon_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 2097152, CC.RESTRICTION_ALLOWED_MIMES :
	GJP } ) )
	fourchan_imageboards.append( Imageboard( '/c/', 'https://sys.4chan.org/c/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/cgl/', 'https://sys.4chan.org/cgl/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/ck/', 'https://sys.4chan.org/ck/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/cm/', 'https://sys.4chan.org/cm/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/co/', 'https://sys.4chan.org/co/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/d/', 'https://sys.4chan.org/d/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/diy/', 'https://sys.4chan.org/diy/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/e/', 'https://sys.4chan.org/e/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/fa/', 'https://sys.4chan.org/fa/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/fit/', 'https://sys.4chan.org/fit/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/g/', 'https://sys.4chan.org/g/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/gd/', 'https://sys.4chan.org/gd/post', 75, fourchan_typical_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 8388608, CC.RESTRICTION_ALLOWED_MIMES :
	[ HC.IMAGE_GIF, HC.IMAGE_PNG, HC.IMAGE_JPEG, HC.APPLICATION_PDF ] } ) )
	fourchan_imageboards.append( Imageboard( '/gif/', 'https://sys.4chan.org/gif/post', 75, fourchan_typical_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE :
	4194304, CC.RESTRICTION_ALLOWED_MIMES : [ HC.IMAGE_GIF ] } ) )
	fourchan_imageboards.append( Imageboard( '/h/', 'https://sys.4chan.org/h/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/hc/', 'https://sys.4chan.org/hc/post', 75, fourchan_typical_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 8388608, CC.RESTRICTION_ALLOWED_MIMES :
	GJP } ) )
	fourchan_imageboards.append( Imageboard( '/hm/', 'https://sys.4chan.org/hm/post', 75, fourchan_typical_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 8388608, CC.RESTRICTION_ALLOWED_MIMES :
	GJP } ) )
	fourchan_imageboards.append( Imageboard( '/hr/', 'https://sys.4chan.org/hr/post', 75, fourchan_typical_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 8388608, CC.RESTRICTION_ALLOWED_MIMES : GJP, CC.RESTRICTION_MIN_RESOLUTION : ( 700, 700 ), CC.RESTRICTION_MAX_RESOLUTION :
	( 10000, 10000 ) } ) )
	fourchan_imageboards.append( Imageboard( '/int/', 'https://sys.4chan.org/int/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/jp/', 'https://sys.4chan.org/jp/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/k/', 'https://sys.4chan.org/k/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/lgbt/', 'https://sys.4chan.org/lgbt/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/lit/', 'https://sys.4chan.org/lit/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/m/', 'https://sys.4chan.org/m/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/mlp/', 'https://sys.4chan.org/mlp/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/mu/', 'https://sys.4chan.org/mu/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/n/', 'https://sys.4chan.org/n/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/o/', 'https://sys.4chan.org/o/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/p/', 'https://sys.4chan.org/p/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/po/', 'https://sys.4chan.org/po/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/pol/', 'https://sys.4chan.org/pol/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/r9k/', 'https://sys.4chan.org/r9k/post', 75, fourchan_spoiler_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 2097152, CC.RESTRICTION_ALLOWED_MIMES :
	GJP } ) )
	fourchan_imageboards.append( Imageboard( '/s/', 'https://sys.4chan.org/s/post', 75, fourchan_typical_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 8388608, CC.RESTRICTION_ALLOWED_MIMES :
	GJP } ) )
	fourchan_imageboards.append( Imageboard( '/sci/', 'https://sys.4chan.org/sci/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/soc/', 'https://sys.4chan.org/soc/post', 75, fourchan_anon_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 2097152, CC.RESTRICTION_ALLOWED_MIMES :
	GJP } ) )
	fourchan_imageboards.append( Imageboard( '/sp/', 'https://sys.4chan.org/sp/post', 75, fourchan_typical_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 4194304, CC.RESTRICTION_ALLOWED_MIMES :
	GJP } ) )
	fourchan_imageboards.append( Imageboard( '/tg/', 'https://sys.4chan.org/tg/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/toy/', 'https://sys.4chan.org/toy/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/trv/', 'https://sys.4chan.org/trv/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/tv/', 'https://sys.4chan.org/tv/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/u/', 'https://sys.4chan.org/u/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/v/', 'https://sys.4chan.org/v/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/vg/', 'https://sys.4chan.org/vg/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/vr/', 'https://sys.4chan.org/vr/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/w/', 'https://sys.4chan.org/w/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/wg/', 'https://sys.4chan.org/wg/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/wsg/', 'https://sys.4chan.org/wsg/post', 75, fourchan_typical_form_fields, { CC.RESTRICTION_MAX_FILE_SIZE : 4194304, CC.RESTRICTION_ALLOWED_MIMES :
	[ HC.IMAGE_GIF ] } ) )
	fourchan_imageboards.append( Imageboard( '/x/', 'https://sys.4chan.org/x/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/y/', 'https://sys.4chan.org/y/post', 75, fourchan_typical_form_fields, fourchan_typical_restrictions ) )
	fourchan_imageboards.append( Imageboard( '/vp/', 'https://sys.4chan.org/vp/post', 75, fourchan_spoiler_form_fields, fourchan_typical_restrictions ) )

	imageboards.append( ( '4chan', fourchan_imageboards ) )

	return imageboards

def GetDefaultLoginScripts():
	dir_path = os.path.join( HC.STATIC_DIR, 'default', 'login_scripts' )

	return GetDefaultObjectsFromPNGs( dir_path, ( LoginScriptDomain, ) )

def GetDefaultParsers():
	dir_path = os.path.join( HC.STATIC_DIR, 'default', 'parsers' )

	return GetDefaultObjectsFromPNGs( dir_path, ( PageParser, ) )

def GetDefaultScriptRows():
	script_info = []

	script_info.append( ( 32, 'gelbooru md5', 1, GetNow(), '''["http://gelbooru.com/index.php", 0, 1, 1, "md5", {"s": "list", "page": "post"}, [[30, 1, ["we got sent back to main gallery page -- title test", 8, [27, 1, [[["head", {}, 0], ["title", {}, 0]], null]], [true, true, "Image List"]]], [30, 1, ["", 0, [27, 1, [[["li", {"class": "tag-type-general"}, null], ["a", {}, 1]], null]], ""]], [30, 1, ["", 0, [27, 1, [[["li", {"class": "tag-type-copyright"}, null], ["a", {}, 1]], null]], "series"]], [30, 1, ["", 0, [27, 1, [[["li", {"class": "tag-type-artist"}, null], ["a", {}, 1]], null]], "creator"]], [30, 1, ["", 0, [27, 1, [[["li", {"class": "tag-type-character"}, null], ["a", {}, 1]], null]], "character"]], [30, 1, ["we got sent back to main gallery page -- page links exist", 8, [27, 1, [[["div", {}, null]], "class"]], [true, true, "pagination"]]]]]''' ) )
	script_info.append( ( 32, 'iqdb danbooru', 1, GetNow(), '''["http://danbooru.iqdb.org/", 1, 0, 0, "file", {}, [[29, 1, ["link to danbooru", [27, 1, [[["td", {"class": "image"}, 1], ["a", {}, 0]], "href"]], [[30, 1, ["", 0, [27, 1, [[["section", {"id": "tag-list"}, 0], ["li", {"class": "category-1"}, null], ["a", {"class": "search-tag"}, 0]], null]], "creator"]], [30, 1, ["", 0, [27, 1, [[["section", {"id": "tag-list"}, 0], ["li", {"class": "category-3"}, null], ["a", {"class": "search-tag"}, 0]], null]], "series"]], [30, 1, ["", 0, [27, 1, [[["section", {"id": "tag-list"}, 0], ["li", {"class": "category-4"}, null], ["a", {"class": "search-tag"}, 0]], null]], "character"]], [30, 1, ["", 0, [27, 1, [[["section", {"id": "tag-list"}, 0], ["li", {"class": "category-0"}, null], ["a", {"class": "search-tag"}, 0]], null]], ""]]]]], [30, 1, ["no iqdb match found", 8, [27, 1, [[["th", {}, null]], null]], [false, true, "Best match"]]]]]''' ) )

	return script_info

def GetDefaultShortcuts():
	shortcuts = []

	archive_delete_filter = Shortcuts( 'archive_delete_filter' )

	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_keep' ) )
	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_RIGHT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_delete' ) )
	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_MIDDLE, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_back' ) )

	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_SPACE, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_keep' ) )
	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F7, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_keep' ) )
	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_DELETE, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_delete' ) )
	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_DELETE, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_delete' ) )
	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_BACK, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_back' ) )
	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_UP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_skip' ) )
	archive_delete_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_UP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_delete_filter_skip' ) )

	shortcuts.append( archive_delete_filter )

	duplicate_filter = Shortcuts( 'duplicate_filter' )

	duplicate_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_this_is_better' ) )
	duplicate_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_RIGHT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_alternates' ) )
	duplicate_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_MIDDLE, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_back' ) )

	duplicate_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_SPACE, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_this_is_better' ) )
	duplicate_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_UP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ) )
	duplicate_filter.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_UP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ) )

	shortcuts.append( duplicate_filter )

	media = Shortcuts( 'media' )

	media.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F4, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'manage_file_ratings' ) )
	media.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F3, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'manage_file_tags' ) )

	media.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F7, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive_file' ) )
	media.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F7, [ CC.SHORTCUT_MODIFIER_SHIFT ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'inbox_file' ) )

	media.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'E' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'open_file_in_external_program' ) )

	media.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'R' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'remove_file_from_view' ) )

	media.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F12, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'launch_the_archive_delete_filter' ) )

	media.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'C' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'copy_file' ) )

	shortcuts.append( media )

	main_gui = Shortcuts( 'main_gui' )

	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F5, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'refresh' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F9, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'new_page' ) )

	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'I' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'synchronised_wait_switch' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'M' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'set_media_focus' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'R' ), [ CC.SHORTCUT_MODIFIER_CTRL, CC.SHORTCUT_MODIFIER_SHIFT ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'show_hide_splitters' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'S' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'set_search_focus' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'T' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'new_page' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'U' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'unclose_page' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'W' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'close_page' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'Y' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'redo' ) )
	main_gui.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'Z' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'undo' ) )

	shortcuts.append( main_gui )

	media_viewer_browser = Shortcuts( 'media_viewer_browser' )

	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_UP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_previous' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_LEFT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_previous' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_UP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_previous' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_LEFT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_previous' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_PAGEUP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_previous' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_PAGEUP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_previous' ) )

	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_SCROLL_UP, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_previous' ) )

	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_DOWN, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_next' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_RIGHT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_next' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_RIGHT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_next' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_DOWN, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_next' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_PAGEDOWN, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_next' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_PAGEDOWN, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_next' ) )

	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_SCROLL_DOWN, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_next' ) )

	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_HOME, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_first' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_HOME, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_first' ) )

	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_END, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_last' ) )
	media_viewer_browser.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_END, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_last' ) )

	shortcuts.append( media_viewer_browser )

	media_viewer = Shortcuts( 'media_viewer' )

	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'B' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'move_animation_to_previous_frame' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'N' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'move_animation_to_next_frame' ) )

	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'F' ), [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'switch_between_fullscreen_borderless_and_regular_framed_window' ) )

	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'Z' ), [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'switch_between_100_percent_and_canvas_zoom' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_ADD, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_in' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_ADD, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_in' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_SUBTRACT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_out' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_SUBTRACT, [] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_out' ) )

	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_SCROLL_UP, [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_in' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_SCROLL_DOWN, [ CC.SHORTCUT_MODIFIER_CTRL ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_out' ) )

	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_UP, [ CC.SHORTCUT_MODIFIER_SHIFT ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'pan_up' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_DOWN, [ CC.SHORTCUT_MODIFIER_SHIFT ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'pan_down' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_LEFT, [ CC.SHORTCUT_MODIFIER_SHIFT ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'pan_left' ) )
	media_viewer.SetCommand( Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_RIGHT, [ CC.SHORTCUT_MODIFIER_SHIFT ] ), ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'pan_right' ) )

	shortcuts.append( media_viewer )

	return shortcuts

def GetDefaultSimpleDownloaderFormulae():
	dir_path = os.path.join( HC.STATIC_DIR, 'default', 'simple_downloader_formulae' )

	return GetDefaultObjectsFromPNGs( dir_path, ( SimpleDownloaderParsingFormula, ) )

def GetDefaultURLMatches():
	dir_path = os.path.join( HC.STATIC_DIR, 'default', 'url_classes' )

	return GetDefaultObjectsFromPNGs( dir_path, ( URLMatch, ) )

def GetDefaultObjectsFromPNGs( dir_path, allowed_object_types ):
	if not os.path.exists( dir_path ):
		return []

	default_objects = []

	for filename in os.listdir( dir_path ):
		path = os.path.join( dir_path, filename )

		try:
			payload = LoadFromPng( path )

			obj = CreateFromNetworkBytes( payload )

			if isinstance( obj, SerialisableList ):
				objs = obj

			else:
				objs = [ obj ]

			for obj in objs:
				if isinstance( obj, allowed_object_types ):
					default_objects.append( obj )

		except Exception:
			pass

	return default_objects

def SetDefaultBandwidthManagerRules( bandwidth_manager ):
	# KB = 1024
	MB = 1024 ** 2
	GB = 1024 ** 3

	#

	rules = BandwidthRules()

	rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 1, 5 ) # stop accidental spam
	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 60, 512 * MB ) # smooth out heavy usage. db and gui prob need a break

	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 10 * GB ) # check your inbox lad

	bandwidth_manager.SetRules( GLOBAL_NETWORK_CONTEXT, rules )

	#

	rules = BandwidthRules()

	rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 1, 1 ) # don't ever hammer a domain

	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 2 * GB ) # don't go nuts on a site in a single day

	bandwidth_manager.SetRules( NetworkContext( CC.NETWORK_CONTEXT_DOMAIN ), rules )

	#

	rules = BandwidthRules()

	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 64 * MB ) # don't sync a giant db in one day

	bandwidth_manager.SetRules( NetworkContext( CC.NETWORK_CONTEXT_HYDRUS ), rules )

	#

	rules = BandwidthRules()

	# most gallery downloaders need two rqs per file (page and file), remember
	rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 300, 200 ) # after that first sample of small files, take it easy

	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 300, 128 * MB ) # after that first sample of big files, take it easy

	bandwidth_manager.SetRules( NetworkContext( CC.NETWORK_CONTEXT_DOWNLOADER_PAGE ), rules )

	#

	rules = BandwidthRules()

	# most gallery downloaders need two rqs per file (page and file), remember
	rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 86400, 400 ) # catch up on a big sub in little chunks every day

	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 256 * MB ) # catch up on a big sub in little chunks every day

	bandwidth_manager.SetRules( NetworkContext( CC.NETWORK_CONTEXT_SUBSCRIPTION ), rules )

	#

	rules = BandwidthRules()

	rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 300, 100 ) # after that first sample of small files, take it easy

	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 300, 128 * MB ) # after that first sample of big files, take it easy

	bandwidth_manager.SetRules( NetworkContext( CC.NETWORK_CONTEXT_WATCHER_PAGE ), rules )

	#

	rules = BandwidthRules()

	rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 60 * 7, 80 )

	rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 4, 1 )

	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 2 * GB ) # keep this in there so subs can know better when to stop running (the files come from a subdomain, which causes a pain for bandwidth calcs)

	rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 64 * MB ) # added as a compromise to try to reduce hydrus sankaku bandwidth usage until their new API and subscription model comes in

	bandwidth_manager.SetRules( NetworkContext( CC.NETWORK_CONTEXT_DOMAIN, 'sankakucomplex.com' ), rules )

def SetDefaultDomainManagerData( domain_manager ):
	network_contexts_to_custom_header_dicts = {}

	#

	custom_header_dict = {}

	custom_header_dict[ 'User-Agent' ] = ( 'Mozilla/5.0 (compatible; Hydrus Client)', VALID_APPROVED, 'This is the default User-Agent identifier for the client for all network connections.' )

	network_contexts_to_custom_header_dicts[ GLOBAL_NETWORK_CONTEXT ] = custom_header_dict

	#

	custom_header_dict = {}

	custom_header_dict[ 'User-Agent' ] = ( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0', VALID_UNKNOWN, 'Sankaku have unusual User-Agent rules on certain requests. Setting this User-Agent allows the sankaku downloader to work.' )

	network_context = NetworkContext( CC.NETWORK_CONTEXT_DOMAIN, 'sankakucomplex.com' )

	network_contexts_to_custom_header_dicts[ network_context ] = custom_header_dict

	#

	domain_manager.SetNetworkContextsToCustomHeaderDicts( network_contexts_to_custom_header_dicts )

	#

	gugs = GetDefaultGUGs()

	domain_manager.SetGUGs( gugs )

	gug_keys_to_display = [ gug.GetGUGKey() for gug in gugs if 'ugoira' not in gug.GetName() ]

	domain_manager.SetGUGKeysToDisplay( gug_keys_to_display )

	#

	domain_manager.SetURLMatches( GetDefaultURLMatches() )

	#

	domain_manager.SetParsers( GetDefaultParsers() )

	#

	domain_manager.TryToLinkURLMatchesAndParsers()

def SetDefaultLoginManagerScripts( login_manager ):
	default_login_scripts = GetDefaultLoginScripts()

	login_manager.SetLoginScripts( default_login_scripts )