code:
import collections

from lib.Hydrus.Data import ContentUpdate
from lib.Hydrus.Exceptions import DataMissing
from lib.Hydrus.Tags.Local import TagFilter
from lib.Hydrus.Serialisable import SerialisableBase, CreateFromSerialisableTuple

### CONSTANTS ###

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

### CLASSES ###

class DuplicateActionOptions( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["DUPLICATE_ACTION_OPTIONS"]
	SERIALISABLE_NAME = 'Duplicate Action Options'
	SERIALISABLE_VERSION = 3

	def __init__( self, tag_service_actions = None, rating_service_actions = None, delete_second_file = False, sync_archive = False, delete_both_files = False, sync_urls_action = None ):
		if tag_service_actions is None:
			tag_service_actions = []

		if rating_service_actions is None:
			rating_service_actions = []

		SerialisableBase.__init__( self )

		self._tag_service_actions = tag_service_actions
		self._rating_service_actions = rating_service_actions
		self._delete_second_file = delete_second_file
		self._sync_archive = sync_archive
		self._delete_both_files = delete_both_files
		self._sync_urls_action = sync_urls_action

	def _GetSerialisableInfo( self ):
		if HG.client_controller.IsBooted():
			services_manager = HG.client_controller.services_manager

			self._tag_service_actions = [ ( service_key, action, tag_filter ) for ( service_key, action, tag_filter ) in self._tag_service_actions if services_manager.ServiceExists( service_key ) and services_manager.GetServiceType( service_key ) in ( HC.LOCAL_TAG, HC.TAG_REPOSITORY ) ]
			self._rating_service_actions = [ ( service_key, action ) for ( service_key, action ) in self._rating_service_actions if services_manager.ServiceExists( service_key ) and services_manager.GetServiceType( service_key ) in ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) ]

		serialisable_tag_service_actions = [ ( service_key.hex(), action, tag_filter.GetSerialisableTuple() ) for ( service_key, action, tag_filter ) in self._tag_service_actions ]
		serialisable_rating_service_actions = [ ( service_key.hex(), action ) for ( service_key, action ) in self._rating_service_actions ]

		return ( serialisable_tag_service_actions, serialisable_rating_service_actions, self._delete_second_file, self._sync_archive, self._delete_both_files, self._sync_urls_action )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( serialisable_tag_service_actions, serialisable_rating_service_actions, self._delete_second_file, self._sync_archive, self._delete_both_files, self._sync_urls_action ) = serialisable_info

		self._tag_service_actions = [ ( bytes.fromhex( serialisable_service_key ), action, CreateFromSerialisableTuple( serialisable_tag_filter ) ) for ( serialisable_service_key, action, serialisable_tag_filter ) in serialisable_tag_service_actions ]
		self._rating_service_actions = [ ( bytes.fromhex( serialisable_service_key ), action ) for ( serialisable_service_key, action ) in serialisable_rating_service_actions ]

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

			tag_service_actions = []
			rating_service_actions = []

			# As the client isn't booted when this is loaded in options, there isn't a good way to figure out tag from rating
			# So, let's just dupe and purge later on, in serialisation
			for ( service_key_encoded, action ) in serialisable_service_actions:
				service_key = bytes.fromhex( service_key_encoded )

				tag_filter = TagFilter()

				tag_service_actions.append( ( service_key, action, tag_filter ) )

				rating_service_actions.append( ( service_key, action ) )

			serialisable_tag_service_actions = [ ( service_key.hex(), action, tag_filter.GetSerialisableTuple() ) for ( service_key, action, tag_filter ) in tag_service_actions ]
			serialisable_rating_service_actions = [ ( service_key.hex(), action ) for ( service_key, action ) in rating_service_actions ]

			sync_archive = delete_second_file
			delete_both_files = False

			new_serialisable_info = ( serialisable_tag_service_actions, serialisable_rating_service_actions, delete_second_file, sync_archive, delete_both_files )

			return ( 2, new_serialisable_info )

		if version == 2:
			( serialisable_tag_service_actions, serialisable_rating_service_actions, delete_second_file, sync_archive, delete_both_files ) = old_serialisable_info

			sync_urls_action = None

			new_serialisable_info = ( serialisable_tag_service_actions, serialisable_rating_service_actions, delete_second_file, sync_archive, delete_both_files, sync_urls_action )

			return ( 3, new_serialisable_info )

	def GetDeletedHashes( self, first_media, second_media ):
		first_hashes = first_media.GetHashes()
		second_hashes = second_media.GetHashes()

		if self._delete_second_file:
			return second_hashes

		elif self._delete_both_files:
			return first_hashes.union( second_hashes )

		else:
			return set()

	def SetTuple( self, tag_service_actions, rating_service_actions, delete_second_file, sync_archive, delete_both_files, sync_urls_action ):
		self._tag_service_actions = tag_service_actions
		self._rating_service_actions = rating_service_actions
		self._delete_second_file = delete_second_file
		self._sync_archive = sync_archive
		self._delete_both_files = delete_both_files
		self._sync_urls_action = sync_urls_action

	def ToTuple( self ):
		return ( self._tag_service_actions, self._rating_service_actions, self._delete_second_file, self._sync_archive, self._delete_both_files, self._sync_urls_action )

	def ProcessPairIntoContentUpdates( self, first_media, second_media, file_deletion_reason = None ):
		if file_deletion_reason is None:
			file_deletion_reason = 'unknown reason'

		service_keys_to_content_updates = collections.defaultdict( list )

		first_hashes = first_media.GetHashes()
		second_hashes = second_media.GetHashes()

		#

		services_manager = HG.client_controller.services_manager

		for ( service_key, action, tag_filter ) in self._tag_service_actions:
			content_updates = []

			try:
				service = services_manager.GetService( service_key )

			except DataMissing:
				continue

			service_type = service.GetServiceType()

			if service_type == HC.LOCAL_TAG:
				add_content_action = HC.CONTENT_UPDATE_ADD

			elif service_type == HC.TAG_REPOSITORY:
				add_content_action = HC.CONTENT_UPDATE_PEND

			first_tags = first_media.GetTagsManager().GetCurrentAndPending( service_key )
			second_tags = second_media.GetTagsManager().GetCurrentAndPending( service_key )

			first_tags = tag_filter.Filter( first_tags )
			second_tags = tag_filter.Filter( second_tags )

			if action == HC.CONTENT_MERGE_ACTION_TWO_WAY_MERGE:
				first_needs = second_tags.difference( first_tags )
				second_needs = first_tags.difference( second_tags )

				content_updates.extend( ( ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, add_content_action, ( tag, first_hashes ) ) for tag in first_needs ) )
				content_updates.extend( ( ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, add_content_action, ( tag, second_hashes ) ) for tag in second_needs ) )

			elif action == HC.CONTENT_MERGE_ACTION_COPY:
				first_needs = second_tags.difference( first_tags )

				content_updates.extend( ( ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, add_content_action, ( tag, first_hashes ) ) for tag in first_needs ) )

			elif service_type == HC.LOCAL_TAG and action == HC.CONTENT_MERGE_ACTION_MOVE:
				first_needs = second_tags.difference( first_tags )

				content_updates.extend( ( ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, add_content_action, ( tag, first_hashes ) ) for tag in first_needs ) )
				content_updates.extend( ( ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_UPDATE_DELETE, ( tag, second_hashes ) ) for tag in second_tags ) )

			if len( content_updates ) > 0:
				service_keys_to_content_updates[ service_key ].extend( content_updates )

		def worth_updating_rating( source_rating, dest_rating ):
			if source_rating is not None:
				if dest_rating is None or source_rating > dest_rating:
					return True

			return False

		for ( service_key, action ) in self._rating_service_actions:
			content_updates = []

			try:
				service = services_manager.GetService( service_key )

			except DataMissing:
				continue

			first_current_value = first_media.GetRatingsManager().GetRating( service_key )
			second_current_value = second_media.GetRatingsManager().GetRating( service_key )

			if action == HC.CONTENT_MERGE_ACTION_TWO_WAY_MERGE:
				if worth_updating_rating( first_current_value, second_current_value ):
					content_updates.append( ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( first_current_value, second_hashes ) ) )

				elif worth_updating_rating( second_current_value, first_current_value ):
					content_updates.append( ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( second_current_value, first_hashes ) ) )

			elif action == HC.CONTENT_MERGE_ACTION_COPY:
				if worth_updating_rating( second_current_value, first_current_value ):
					content_updates.append( ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( second_current_value, first_hashes ) ) )

			elif action == HC.CONTENT_MERGE_ACTION_MOVE:
				if second_current_value is not None:
					if worth_updating_rating( second_current_value, first_current_value ):
						content_updates.append( ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( second_current_value, first_hashes ) ) )

					content_updates.append( ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( None, second_hashes ) ) )

			if len( content_updates ) > 0:
				service_keys_to_content_updates[ service_key ].extend( content_updates )

		#

		if self._sync_archive:
			if first_media.HasInbox() and second_media.HasArchive():
				content_update = ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, first_hashes )

				service_keys_to_content_updates[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ].append( content_update )

			elif first_media.HasArchive() and second_media.HasInbox():
				content_update = ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, second_hashes )

				service_keys_to_content_updates[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ].append( content_update )

		#

		if self._sync_urls_action is not None:
			first_urls = set( first_media.GetLocationsManager().GetURLs() )
			second_urls = set( second_media.GetLocationsManager().GetURLs() )

			content_updates = []

			if self._sync_urls_action == HC.CONTENT_MERGE_ACTION_TWO_WAY_MERGE:
				first_needs = second_urls.difference( first_urls )
				second_needs = first_urls.difference( second_urls )

				if len( first_needs ) > 0:
					content_updates.append( ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( first_needs, first_hashes ) ) )

				if len( second_needs ) > 0:
					content_updates.append( ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( second_needs, second_hashes ) ) )

			elif self._sync_urls_action == HC.CONTENT_MERGE_ACTION_COPY:
				first_needs = second_urls.difference( first_urls )

				if len( first_needs ) > 0:
					content_updates.append( ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( first_needs, first_hashes ) ) )

			if len( content_updates ) > 0:
				service_keys_to_content_updates[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ].extend( content_updates )

		#

		deletee_media = []

		if self._delete_second_file or self._delete_both_files:
			if self._delete_both_files:
				deletee_media.append( first_media )

				file_deletion_reason += ': both files deleted'

			else:
				file_deletion_reason += ': \'worse\' file deleted'

			deletee_media.append( second_media )

		for media in deletee_media:
			current_locations = media.GetLocationsManager().GetCurrent()

			if CC.LOCAL_FILE_SERVICE_KEY in current_locations:
				deletee_service_key = CC.LOCAL_FILE_SERVICE_KEY

			elif CC.TRASH_SERVICE_KEY in current_locations:
				deletee_service_key = CC.TRASH_SERVICE_KEY

			else:
				deletee_service_key = None

			if deletee_service_key is not None:
				content_update = ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, media.GetHashes(), reason = file_deletion_reason )

				service_keys_to_content_updates[ deletee_service_key ].append( content_update )

		#

		return service_keys_to_content_updates

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["DUPLICATE_ACTION_OPTIONS"] ] = DuplicateActionOptions


code:
import OpenSSL, os, stat

### FUNCTIONS

def GenerateOpenSSLCertAndKeyFile( cert_path, key_path ):
	key = OpenSSL.crypto.PKey()

	key.generate_key( OpenSSL.crypto.TYPE_RSA, 2048 )

	# create a self-signed cert
	cert = OpenSSL.crypto.X509()

	cert.get_subject().countryName = 'HN'
	cert.get_subject().organizationName = 'hydrus network'
	cert.get_subject().organizationalUnitName = os.urandom( 32 ).hex()
	cert.set_serial_number( 1 )
	cert.gmtime_adj_notBefore( 0 )
	cert.gmtime_adj_notAfter( 10*365*24*60*60 )
	cert.set_issuer( cert.get_subject() )
	cert.set_pubkey( key )
	cert.sign( key, 'sha256' )

	cert_bytes = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert )

	with open( cert_path, 'wb' ) as f:
		f.write( cert_bytes )

	os.chmod( cert_path, stat.S_IREAD )

	key_bytes = OpenSSL.crypto.dump_privatekey( OpenSSL.crypto.FILETYPE_PEM, key )

	with open( key_path, 'wb' ) as f:
		f.write( key_bytes )

	os.chmod( key_path, stat.S_IREAD )

''' old crypto code experiments

	import Crypto.Cipher.AES
	import Crypto.Cipher.PKCS1_OAEP
	import Crypto.PublicKey.RSA

	AES_KEY_LENGTH = 32
	AES_BLOCK_SIZE = 16

	def DecryptAES( aes_key, encrypted_message ):
		iv = encrypted_message[:AES_BLOCK_SIZE]
		enciphered_message = encrypted_message[AES_BLOCK_SIZE:]

		aes_cipher = Crypto.Cipher.AES.new( aes_key, Crypto.Cipher.AES.MODE_CFB, iv )

		padded_message = aes_cipher.decrypt( enciphered_message )

		message = UnpadAES( padded_message )

		return message

	def DecryptAESStream( aes_key, stream_in, stream_out ):
		iv = stream_in.read( AES_BLOCK_SIZE )

		aes_cipher = Crypto.Cipher.AES.new( aes_key, Crypto.Cipher.AES.MODE_CFB, iv )

		next_block = stream_in.read( HC.READ_BLOCK_SIZE )

		while True:
			block = next_block

			next_block = stream_in.read( HC.READ_BLOCK_SIZE )

			decrypted_block = aes_cipher.decrypt( block )

			if next_block == '':
				decrypted_block = UnpadAES( decrypted_block )

			stream_out.write( decrypted_block )

			if next_block == '':
				break

	def DecryptPKCS( private_key, encrypted_message ):
		rsa_cipher = Crypto.Cipher.PKCS1_OAEP.new( private_key )

		message = rsa_cipher.decrypt( encrypted_message )

		return message

	def DeserialiseRSAKey( text ):
		return Crypto.PublicKey.RSA.importKey( text )

	def EncryptAES( aes_key, message ):
		iv = GenerateIV()

		padded_message = PadAES( message )

		aes_cipher = Crypto.Cipher.AES.new( aes_key, Crypto.Cipher.AES.MODE_CFB, iv )

		enciphered_message = aes_cipher.encrypt( padded_message )

		encrypted_message = iv + enciphered_message

		return encrypted_message

	def EncryptAESStream( aes_key, stream_in, stream_out ):
		iv = GenerateIV()

		stream_out.write( iv )

		aes_cipher = Crypto.Cipher.AES.new( aes_key, Crypto.Cipher.AES.MODE_CFB, iv )

		next_block = stream_in.read( HC.READ_BLOCK_SIZE )

		while True:
			block = next_block

			next_block = stream_in.read( HC.READ_BLOCK_SIZE )

			if next_block == '':
				block = PadAES( block )

			encrypted_block = aes_cipher.encrypt( block )

			stream_out.write( encrypted_block )

			if next_block == '':
				break

	def EncryptPKCS( public_key, message ):
		rsa_cipher = Crypto.Cipher.PKCS1_OAEP.new( public_key )

		# my understanding is that I don't have to manually pad this, cause OAEP does it for me.
		encrypted_message = rsa_cipher.encrypt( message )

		return encrypted_message

	def GenerateAESKey():
		return os.urandom( AES_KEY_LENGTH )

	def GenerateIV():
		return os.urandom( AES_BLOCK_SIZE )

	def GenerateFilteredRandomBytes( byte_to_exclude, num_bytes ):
		bytes = []

		while len( bytes ) < num_bytes:
			new_byte = os.urandom( 1 )

			if new_byte != byte_to_exclude:
				bytes.append( new_byte )

		return ''.join( bytes )

	def GenerateRSAKeyPair():
		private_key = Crypto.PublicKey.RSA.generate( 2048 )

		public_key = private_key.publickey()

		return ( private_key, public_key )

	def PadAES( message ):
		block_size = AES_BLOCK_SIZE

		# get last byte
		# add random gumpf (except for last byte), then add last byte again

		last_byte = message[-1]

		num_bytes_to_add = block_size - ( len( message ) % block_size )

		pad = GenerateFilteredRandomBytes( last_byte, num_bytes_to_add - 1 ) + last_byte

		return message + pad

	def SerialiseRSAKey( key ):
		return key.exportKey()

	def UnpadAES( message ):
		block_size = AES_BLOCK_SIZE

		# check last byte, jump back to previous instance of that byte

		last_byte = message[-1]

		i = 2

		while True:
			if message[-i] == last_byte:
				break

			i += 1

		index_of_correct_end = len( message ) - i

		return message[:index_of_correct_end + 1]
'''

license: wtfpl


code:
import os

### CLASSES ###

class HydrusExceptionBase( Exception ):
	def __str__( self ):
		return os.linesep.join( self.args )

class CantRenderWithCVException( HydrusExceptionBase ):
	pass

class DataMissing( HydrusExceptionBase ):
	pass

class DBException( HydrusExceptionBase ):
	pass

class DBAccessException( HydrusExceptionBase ):
	pass

class FileMissingException( HydrusExceptionBase ):
	pass

class SerialisationException( HydrusExceptionBase ):
	pass

class NameException( HydrusExceptionBase ):
	pass

class ShutdownException( HydrusExceptionBase ):
	pass

class WXDeadWindowException( HydrusExceptionBase ):
	pass

class VetoException( HydrusExceptionBase ):
	pass

class CancelledException( VetoException ):
	pass

class MimeException( VetoException ):
	pass

class SizeException( VetoException ):
	pass

class DecompressionBombException( SizeException ):
	pass

class ParseException( HydrusExceptionBase ):
	pass

class StringConvertException( ParseException ):
	pass

class StringMatchException( ParseException ):
	pass

class URLMatchException( ParseException ):
	pass

class GUGException( ParseException ):
	pass

class NetworkException( HydrusExceptionBase ):
	pass

class NetworkInfrastructureException( NetworkException ):
	pass

class ConnectionException( NetworkInfrastructureException ):
	pass

class FirewallException( NetworkInfrastructureException ):
	pass

class ServerBusyException( NetworkInfrastructureException ):
	pass

class BandwidthException( NetworkException ):
	pass

class NetworkVersionException( NetworkException ):
	pass

class NoContentException( NetworkException ):
	pass

class NotFoundException( NetworkException ):
	pass

class NotModifiedException( NetworkException ):
	pass

class BadRequestException( NetworkException ):
	pass

class MissingCredentialsException( NetworkException ):
	pass

class DoesNotSupportCORSException( NetworkException ):
	pass

class InsufficientCredentialsException( NetworkException ):
	pass

class RedirectionException( NetworkException ):
	pass

class ServerException( NetworkException ):
	pass

class SessionException( NetworkException ):
	pass

class WrongServiceTypeException( NetworkException ):
	pass

class ValidationException( NetworkException ):
	pass

class ShouldReattemptNetworkException( NetworkException ):
	pass


code:
import os

from lib.Hydrus.Data import HumanTextSort
from lib.Hydrus.Exceptions import ShutdownException

### CONSTANTS ###

import lib.Hydrus.Globals as HG

REGENERATE_FILE_DATA_JOB_COMPLETE = 0
REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL = 1
REGENERATE_FILE_DATA_JOB_REFIT_THUMBNAIL = 2

### FUNCTIONS ###

def GetAllPaths( raw_paths, do_human_sort = True ):
	file_paths = []

	paths_to_process = raw_paths

	while len( paths_to_process ) > 0:
		next_paths_to_process = []

		for path in paths_to_process:
			if HG.view_shutdown:
				raise ShutdownException()

			if os.path.isdir( path ):
				subpaths = [ os.path.join( path, filename ) for filename in os.listdir( path ) ]

				next_paths_to_process.extend( subpaths )

			else:
				file_paths.append( path )

		paths_to_process = next_paths_to_process

	if do_human_sort:
		HumanTextSort( file_paths )

	return file_paths


code:
import os

from lib.Hydrus.Data import IterateHexPrefixes
from lib.Hydrus.Exceptions import NotFoundException

### CONSTANTS ###

import lib.Hydrus.Globals as HG

### FUNCTIONS ###

def GetAllHashes( file_type ):
	return { bytes.fromhex( os.path.split( path )[1] ) for path in IterateAllPaths( file_type ) }

def GetExpectedFilePath( hash ):
	files_dir = HG.server_controller.GetFilesDir()

	hash_encoded = hash.hex()

	first_two_chars = hash_encoded[:2]

	path = os.path.join( files_dir, first_two_chars, hash_encoded )

	return path

def GetExpectedThumbnailPath( hash ):
	return GetExpectedFilePath(hash) + ".thumbnail"

def GetFilePath( hash ):
	path = GetExpectedFilePath( hash )

	if not os.path.exists( path ):
		raise NotFoundException( 'File not found!' )

	return path

def GetThumbnailPath( hash ):
	path = GetExpectedThumbnailPath( hash )

	if not os.path.exists( path ):
		raise NotFoundException( 'Thumbnail not found!' )

	return path

def IterateAllPaths( file_type ):
	files_dir = HG.server_controller.GetFilesDir()

	for prefix in IterateHexPrefixes():
		dir = os.path.join( files_dir, prefix )
		filenames = os.listdir( dir )

		for filename in filenames:
			if file_type == 'file' and filename.endswith( '.thumbnail' ):
				continue

			elif file_type == 'thumbnail' and not filename.endswith( '.thumbnail' ):
				continue

			yield os.path.join( dir, filename )


code:
__all__ = [
	"Local",
	"Remote"
]

from . import *


code:
import threading

### CONSTANTS ###
controller = None
client_controller = None
server_controller = None
test_controller = None
view_shutdown = False
model_shutdown = False

no_daemons = False
no_wal = False
no_db_temp_files = False

import_folders_running = False
export_folders_running = False
subscriptions_running = False

callto_report_mode = False
db_report_mode = False
db_profile_mode = False
file_report_mode = False
media_load_report_mode = False
gui_report_mode = False
shortcut_report_mode = False
subprocess_report_mode = False
subscription_report_mode = False
hover_window_report_mode = False
menu_profile_mode = False
network_report_mode = False
pubsub_report_mode = False
pubsub_profile_mode = False
ui_timer_profile_mode = False
daemon_report_mode = False
force_idle_mode = False
no_page_limit_mode = False
thumbnail_debug_mode = False
server_busy = False

do_idle_shutdown_work = False
shutdown_complete = False
restart = False
emergency_exit = False

twisted_is_broke = False

do_not_catch_char_hook = False

dirty_object_lock = threading.Lock()


code:
import os, sys, threading, time

### CLASSES ###

class HydrusLogger( object ):
	def __init__( self, db_dir, prefix ):
		self._db_dir = db_dir
		self._prefix = prefix

		self._log_path_base = self._GetLogPathBase()
		self._lock = threading.Lock()

	def __enter__( self ):
		self._previous_sys_stdout = sys.stdout
		self._previous_sys_stderr = sys.stderr

		self._problem_with_previous_stdout = False

		self._OpenLog()

		sys.stdout = self
		sys.stderr = self

		return self

	def __exit__( self, exc_type, exc_val, exc_tb ):
		#sys.stdout = self._previous_sys_stdout
		#sys.stderr = self._previous_sys_stderr

		self._CloseLog()

		return False

	def _CloseLog( self ):
		self._log_file.close()

	def _GetLogPath( self ):
		current_time_struct = time.localtime()

		( current_year, current_month ) = ( current_time_struct.tm_year, current_time_struct.tm_mon )

		log_path = self._log_path_base + ' - ' + str( current_year ) + '-' + str( current_month ) + '.log'

		return log_path

	def _GetLogPathBase( self ):
		return os.path.join( self._db_dir, self._prefix )

	def _OpenLog( self ):
		self._log_path = self._GetLogPath()

		is_new_file = not os.path.exists( self._log_path )

		self._log_file = open( self._log_path, 'a', encoding = 'utf-8' )

		if is_new_file:
			self._log_file.write( u'\uFEFF' ) # Byte Order Mark, BOM, to help reader software interpret this as utf-8

	def _SwitchToANewLogFileIfDue( self ):
		correct_log_path = self._GetLogPath()

		if correct_log_path != self._log_path:
			self._CloseLog()

			self._OpenLog()

	def flush( self ):
		with self._lock:
			if not self._problem_with_previous_stdout:
				try:
					self._previous_sys_stdout.flush()

				except IOError:
					self._problem_with_previous_stdout = True

			self._log_file.flush()

			self._SwitchToANewLogFileIfDue()

	def isatty( self ):
		return False

	def write( self, value ):
		with self._lock:
			if value in ( os.linesep, '\n' ):
				prefix = ''

			else:
				prefix = time.strftime( '%Y/%m/%d %H:%M:%S: ' )

			message = prefix + value

			if not self._problem_with_previous_stdout:
				try:
					self._previous_sys_stdout.write( message )

				except Exception:
					self._problem_with_previous_stdout = True

			self._log_file.write( message )


code:
import lib.Hydrus.Globals as HG

### INDEXES ###

HG.ClientApiPermissions = {
	"ADD_URLS": 0,
	"ADD_FILES": 1,
	"ADD_TAGS": 2,
	"SEARCH_FILES": 3
}

HG.SerialisableType = {
	"BASE": 0,
	"BASE_NAMED": 1,
	"SHORTCUTS": 2,
	"SUBSCRIPTION": 3,
	"PERIODIC": 4,
	"GALLERY_IDENTIFIER": 5,
	"TAG_IMPORT_OPTIONS": 6,
	"FILE_IMPORT_OPTIONS": 7,
	"FILE_SEED_CACHE": 8,
	"HDD_IMPORT": 9,
	"SERVER_TO_CLIENT_CONTENT_UPDATE_PACKAGE": 10,
	"SERVER_TO_CLIENT_SERVICE_UPDATE_PACKAGE": 11,
	"MANAGEMENT_CONTROLLER": 12,
	"GUI_SESSION": 13,
	"PREDICATE": 14,
	"FILE_SEARCH_CONTEXT": 15,
	"EXPORT_FOLDER": 16,
	"WATCHER_IMPORT": 17,
	"SIMPLE_DOWNLOADER_IMPORT": 18,
	"IMPORT_FOLDER": 19,
	"MULTIPLE_GALLERY_IMPORT": 20,
	"DICTIONARY": 21,
	"CLIENT_OPTIONS": 22,
	"CONTENT": 23,
	"PETITION": 24,
	"ACCOUNT_IDENTIFIER": 25,
	"LIST": 26,
	"PARSE_FORMULA_HTML": 27,
	"URLS_IMPORT": 28,
	"PARSE_NODE_CONTENT_LINK": 29,
	"CONTENT_PARSER": 30,
	"PARSE_FORMULA_JSON": 31,
	"PARSE_ROOT_FILE_LOOKUP": 32,
	"BYTES_DICT": 33,
	"CONTENT_UPDATE": 34,
	"CREDENTIALS": 35,
	"DEFINITIONS_UPDATE": 36,
	"METADATA": 37,
	"BANDWIDTH_RULES": 38,
	"BANDWIDTH_TRACKER": 39,
	"CLIENT_TO_SERVER_UPDATE": 40,
	"SHORTCUT": 41,
	"APPLICATION_COMMAND": 42,
	"DUPLICATE_ACTION_OPTIONS": 43,
	"TAG_FILTER": 44,
	"NETWORK_BANDWIDTH_MANAGER": 45,
	"NETWORK_SESSION_MANAGER": 46,
	"NETWORK_CONTEXT": 47,
	"NETWORK_LOGIN_MANAGER": 48,
	"MEDIA_SORT": 49,
	"URL_MATCH": 50,
	"STRING_MATCH": 51,
	"CHECKER_OPTIONS": 52,
	"NETWORK_DOMAIN_MANAGER": 53,
	"SUBSCRIPTION_QUERY": 54,
	"STRING_CONVERTER": 55,
	"FILENAME_TAGGING_OPTIONS": 56,
	"FILE_SEED": 57,
	"PAGE_PARSER": 58,
	"PARSE_FORMULA_COMPOUND": 59,
	"PARSE_FORMULA_CONTEXT_VARIABLE": 60,
	"TAG_SUMMARY_GENERATOR": 61,
	"PARSE_RULE_HTML": 62,
	"SIMPLE_DOWNLOADER_PARSE_FORMULA": 63,
	"MULTIPLE_WATCHER_IMPORT": 64,
	"SERVICE_TAG_IMPORT_OPTIONS": 65,
	"GALLERY_SEED": 66,
	"GALLERY_SEED_LOG": 67,
	"GALLERY_IMPORT": 68,
	"GALLERY_URL_GENERATOR": 69,
	"NESTED_GALLERY_URL_GENERATOR": 70,
	"DOMAIN_METADATA_PACKAGE": 71,
	"LOGIN_CREDENTIAL_DEFINITION": 72,
	"LOGIN_SCRIPT_DOMAIN": 73,
	"LOGIN_STEP": 74,
	"CLIENT_API_MANAGER": 75,
	"CLIENT_API_PERMISSIONS": 76,
	"SERVICE_KEYS_TO_TAGS": 77
}

HG.Ratings = {
	"LIKE": 0,
	"DISLIKE": 1,
	"NULL": 2,
	"SET": 3,
	"MIXED": 4
}

### TABLES ###

HG.LookUpTables = {
	"SERIALISABLE_TYPES_TO_OBJECT_TYPES": {},
	"BASIC_PERMISSION_TO_STR_LOOKUP": {},

	"DEFAULT_NUMERICAL_COLOURS": {},

}


code:
import wx

### CONSTANTS ###
import lib.Hydrus.Constants as HC
import lib.Hydrus.Globals as HG

HG.LookupTables[ "DEFAULT_LIKE_COLOURS" ] = {
	HG.Ratings[ "LIKE" ]: ( ( 0, 0, 0 ), ( 80, 200, 120 ) ),
	HG.Ratings[ "DISLIKE" ]: ( ( 0, 0, 0 ), ( 200, 80, 120 ) ),
	HG.Ratings[ "NULL" ]: ( ( 0, 0, 0 ), ( 191, 191, 191 ) ),
	HG.Ratings[ "MIXED" ]: ( ( 0, 0, 0 ), ( 95, 95, 95 ) ),
}

DEFAULT_NUMERICAL_COLOURS[ HG.Ratings[ "LIKE" ] ] = ( ( 0, 0, 0 ), ( 80, 200, 120 ) )
DEFAULT_NUMERICAL_COLOURS[ HG.Ratings[ "DISLIKE" ] ] = ( ( 0, 0, 0 ), ( 255, 255, 255 ) )
DEFAULT_NUMERICAL_COLOURS[ HG.Ratings[ "NULL" ] ] = ( ( 0, 0, 0 ), ( 191, 191, 191 ) )
DEFAULT_NUMERICAL_COLOURS[ HG.Ratings[ "MIXED" ] ] = ( ( 0, 0, 0 ), ( 95, 95, 95 ) )

STAR_COORDS = []

STAR_COORDS.append( wx.Point( 6, 0 ) ) # top
STAR_COORDS.append( wx.Point( 9, 4 ) )
STAR_COORDS.append( wx.Point( 12, 4 ) ) # right
STAR_COORDS.append( wx.Point( 9, 8 ) )
STAR_COORDS.append( wx.Point( 10, 12 ) ) # bottom right
STAR_COORDS.append( wx.Point( 6, 10 ) )
STAR_COORDS.append( wx.Point( 2, 12 ) ) # bottom left
STAR_COORDS.append( wx.Point( 3, 8 ) )
STAR_COORDS.append( wx.Point( 0, 4 ) ) # left
STAR_COORDS.append( wx.Point( 3, 4 ) )


code:
import collections, random, itertools

from lib.Hydrus.Ratings import RatingsManager
from lib.Hydrus.Exceptions import DataMissing
from lib.Hydrus.Tags import SplitTag
from lib.Hydrus.Data import BuildKeyToListDict, BuildKeyToSetDict, \
	ConvertMillisecondsToPrettyTime, default_dict_set, GenerateKey, \
	GetNow, IntelligentMassIntersect, TimeDeltaToPrettyTimeDelta, \
	ToHumanBytes, ToHumanInt, ConvertTagToSortable, TimestampToPrettyTimeDelta
from lib.Hydrus.Serialisable import SerialisableBase

### CONSTANTS ###

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

### FUNCTIONS ###

def FlattenMedia( media_list ):
	flat_media = []

	for media in media_list:
		if media.IsCollection():
			flat_media.extend( media.GetFlatMedia() )

		else:
			flat_media.append( media )

	return flat_media

def GetDuplicateComparisonStatements( shown_media, comparison_media ):
	new_options = HG.client_controller.new_options

	duplicate_comparison_score_higher_filesize = new_options.GetInteger( 'duplicate_comparison_score_higher_filesize' )
	duplicate_comparison_score_much_higher_filesize = new_options.GetInteger( 'duplicate_comparison_score_much_higher_filesize' )
	duplicate_comparison_score_higher_resolution = new_options.GetInteger( 'duplicate_comparison_score_higher_resolution' )
	duplicate_comparison_score_much_higher_resolution = new_options.GetInteger( 'duplicate_comparison_score_much_higher_resolution' )
	duplicate_comparison_score_more_tags = new_options.GetInteger( 'duplicate_comparison_score_more_tags' )
	duplicate_comparison_score_older = new_options.GetInteger( 'duplicate_comparison_score_older' )

	#

	statements = []
	score = 0

	# size

	s_size = shown_media.GetSize()
	c_size = comparison_media.GetSize()

	size_ratio = s_size / c_size

	if size_ratio > 2.0:
		statements.append( 'This has a much larger filesize.' )

		score += duplicate_comparison_score_much_higher_filesize

	elif size_ratio > 1.05:
		statements.append( 'This has a larger filesize.' )

		score += duplicate_comparison_score_higher_filesize

	elif size_ratio < 0.5:
		statements.append( 'This has a much smaller filesize.' )

		score -= duplicate_comparison_score_more_tags

	elif size_ratio < 0.95:
		statements.append( 'This has a smaller filesize.' )

		score -= duplicate_comparison_score_higher_filesize

	# higher/same res

	s_resolution = shown_media.GetResolution()
	c_resolution = comparison_media.GetResolution()

	if s_resolution is not None and c_resolution is not None:
		( s_w, s_h ) = shown_media.GetResolution()
		( c_w, c_h ) = comparison_media.GetResolution()

		resolution_ratio = ( s_w * s_h ) / ( c_w * c_h )

		if resolution_ratio == 1.0:
			if s_resolution != c_resolution:
				statements.append( 'The files have the same number of pixels but different resolution.' )

		elif resolution_ratio > 2.0:
			statements.append( 'This has much higher resolution.' )

			score += duplicate_comparison_score_much_higher_resolution

		elif resolution_ratio > 1.0:
			statements.append( 'This has higher resolution.' )

			score += duplicate_comparison_score_higher_resolution

		elif resolution_ratio < 0.5:
			statements.append( 'This has much lower resolution.' )

			score -= duplicate_comparison_score_much_higher_resolution

		elif resolution_ratio < 1.0:
			statements.append( 'This has lower resolution.' )

			score -= duplicate_comparison_score_higher_resolution

	# same/diff mime

	s_mime = shown_media.GetMime()
	c_mime = comparison_media.GetMime()

	if s_mime != c_mime:
		statements.append( 'This is ' + HC.mime_string_lookup[ s_mime ] + ', the other is ' + HC.mime_string_lookup[ c_mime ] + '.' )

	# more tags

	s_num_tags = len( shown_media.GetTagsManager().GetCurrentAndPending() )
	c_num_tags = len( comparison_media.GetTagsManager().GetCurrentAndPending() )

	if s_num_tags > 0 and c_num_tags > 0:
		if s_num_tags > c_num_tags:
			statements.append( 'This has more tags.' )

			score += duplicate_comparison_score_more_tags

		elif s_num_tags < c_num_tags:
			statements.append( 'This has fewer tags.' )

			score += duplicate_comparison_score_more_tags

	elif s_num_tags > 0:
		statements.append( 'This has tags, the other does not.' )

	elif c_num_tags > 0:
		statements.append( 'This has no tags, the other does.' )

	# older

	s_ts = shown_media.GetLocationsManager().GetTimestamp( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
	c_ts = comparison_media.GetLocationsManager().GetTimestamp( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )

	if s_ts is not None and c_ts is not None:
		if s_ts < c_ts - 86400 * 30:
			statements.append( 'This is older.' )

			score += duplicate_comparison_score_older

		elif c_ts < s_ts - 86400 * 30:
			statements.append( 'This is newer.' )

			score -= duplicate_comparison_score_older

	return ( statements, score )

def MergeTagsManagers( tags_managers ):
	def CurrentAndPendingFilter( items ):
		for ( service_key, statuses_to_tags ) in items:
			filtered = { status : tags for ( status, tags ) in list( statuses_to_tags.items() ) if status in ( HC.CONTENT_STATUS_CURRENT, HC.CONTENT_STATUS_PENDING ) }

			yield ( service_key, filtered )

	# [[( service_key, statuses_to_tags )]]
	s_k_s_t_t_tupled = ( CurrentAndPendingFilter( list( tags_manager.GetServiceKeysToStatusesToTags().items() ) ) for tags_manager in tags_managers )

	# [( service_key, statuses_to_tags )]
	flattened_s_k_s_t_t = itertools.chain.from_iterable( s_k_s_t_t_tupled )

	# service_key :	[ statuses_to_tags ]
	s_k_s_t_t_dict = BuildKeyToListDict( flattened_s_k_s_t_t )

	# now let's merge so we have service_key : statuses_to_tags

	merged_service_keys_to_statuses_to_tags = collections.defaultdict( default_dict_set )

	for ( service_key, several_statuses_to_tags ) in list( s_k_s_t_t_dict.items() ):
		# [[( status, tags )]]
		s_t_t_tupled = ( list( s_t_t.items() ) for s_t_t in several_statuses_to_tags )

		# [( status, tags )]
		flattened_s_t_t = itertools.chain.from_iterable( s_t_t_tupled )

		statuses_to_tags = default_dict_set()

		for ( status, tags ) in flattened_s_t_t:
				statuses_to_tags[ status ].update( tags )

		merged_service_keys_to_statuses_to_tags[ service_key ] = statuses_to_tags

	return TagsManager( merged_service_keys_to_statuses_to_tags )

class DuplicatesManager( object ):
	def __init__( self, service_keys_to_dupe_statuses_to_counts ):

		self._service_keys_to_dupe_statuses_to_counts = service_keys_to_dupe_statuses_to_counts

	def Duplicate( self ):
		service_keys_to_dupe_statuses_to_counts = collections.defaultdict( collections.Counter )

		return DuplicatesManager( service_keys_to_dupe_statuses_to_counts )

	def GetDupeStatusesToCounts( self, service_key ):
		return self._service_keys_to_dupe_statuses_to_counts[ service_key ]

class FileInfoManager( object ):
	def __init__( self, hash_id, hash, size = None, mime = None, width = None, height = None, duration = None, num_frames = None, num_words = None ):

		if mime is None:
			mime = HC.APPLICATION_UNKNOWN

		self.hash_id = hash_id
		self.hash = hash
		self.size = size
		self.mime = mime
		self.width = width
		self.height = height
		self.duration = duration
		self.num_frames = num_frames
		self.num_words = num_words

	def Duplicate( self ):
		return FileInfoManager( self.hash_id, self.hash, self.size, self.mime, self.width, self.height, self.duration, self.num_frames, self.num_words )

	def ToTuple( self ):
		return ( self.hash_id, self.hash, self.size, self.mime, self.width, self.height, self.duration, self.num_frames, self.num_words )

class FileViewingStatsManager( object ):
	def __init__( self, preview_views, preview_viewtime, media_views, media_viewtime ):

		self.preview_views = preview_views
		self.preview_viewtime = preview_viewtime
		self.media_views = media_views
		self.media_viewtime = media_viewtime

	def Duplicate( self ):
		return FileViewingStatsManager( self.preview_views, self.preview_viewtime, self.media_views, self.media_viewtime )

	def GetPrettyCombinedLine( self ):
		return 'viewed ' + ToHumanInt( self.media_views + self.preview_views ) + ' times, totalling ' + TimeDeltaToPrettyTimeDelta( self.media_viewtime + self.preview_viewtime )

	def GetPrettyMediaLine( self ):
		return 'viewed ' + ToHumanInt( self.media_views ) + ' times in media viewer, totalling ' + TimeDeltaToPrettyTimeDelta( self.media_viewtime )

	def GetPrettyPreviewLine( self ):
		return 'viewed ' + ToHumanInt( self.preview_views ) + ' times in preview window, totalling ' + TimeDeltaToPrettyTimeDelta( self.preview_viewtime )

	def ProcessContentUpdate( self, content_update ):
		( data_type, action, row ) = content_update.ToTuple()

		if action == HC.CONTENT_UPDATE_ADD:
			( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row

			self.preview_views += preview_views_delta
			self.preview_viewtime += preview_viewtime_delta
			self.media_views += media_views_delta
			self.media_viewtime += media_viewtime_delta

	@staticmethod

	def STATICGenerateEmptyManager():
		return FileViewingStatsManager( 0, 0, 0, 0 )

class LocationsManager( object ):
	LOCAL_LOCATIONS = { CC.LOCAL_FILE_SERVICE_KEY, CC.TRASH_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY }

	def __init__( self, current, deleted, pending, petitioned, inbox = False, urls = None, service_keys_to_filenames = None, current_to_timestamps = None ):
		self._current = current
		self._deleted = deleted
		self._pending = pending
		self._petitioned = petitioned

		self._inbox = inbox

		if urls is None:
			urls = set()

		self._urls = urls

		if service_keys_to_filenames is None:
			service_keys_to_filenames = {}

		self._service_keys_to_filenames = service_keys_to_filenames

		if current_to_timestamps is None:
			current_to_timestamps = {}

		self._current_to_timestamps = current_to_timestamps

	def DeletePending( self, service_key ):
		self._pending.discard( service_key )
		self._petitioned.discard( service_key )

	def Duplicate( self ):
		current = set( self._current )
		deleted = set( self._deleted )
		pending = set( self._pending )
		petitioned = set( self._petitioned )
		urls = set( self._urls )
		service_keys_to_filenames = dict( self._service_keys_to_filenames )
		current_to_timestamps = dict( self._current_to_timestamps )

		return LocationsManager( current, deleted, pending, petitioned, self._inbox, urls, service_keys_to_filenames, current_to_timestamps )

	def GetCDPP( self ):
			return ( self._current, self._deleted, self._pending, self._petitioned )

	def GetCurrent( self ):
		return self._current

	def GetCurrentRemote( self ):
		return self._current - self.LOCAL_LOCATIONS

	def GetDeleted( self ):
		return self._deleted

	def GetDeletedRemote( self ):
		return self._deleted - self.LOCAL_LOCATIONS

	def GetInbox( self ):
		return self._inbox

	def GetPending( self ):
		return self._pending

	def GetPendingRemote( self ):
		return self._pending - self.LOCAL_LOCATIONS

	def GetPetitioned( self ):
		return self._petitioned

	def GetPetitionedRemote( self ):
		return self._petitioned - self.LOCAL_LOCATIONS

	def GetRemoteLocationStrings( self ):
		current = self.GetCurrentRemote()
		pending = self.GetPendingRemote()
		petitioned = self.GetPetitionedRemote()

		remote_services = HG.client_controller.services_manager.GetServices( ( HC.FILE_REPOSITORY, HC.IPFS ) )

		remote_services = list( remote_services )

		def key( s ):
			return s.GetName()

		remote_services.sort( key = key )

		remote_service_strings = []

		for remote_service in remote_services:
			name = remote_service.GetName()
			service_key = remote_service.GetServiceKey()

			if service_key in pending:
				remote_service_strings.append( name + ' (+)' )

			elif service_key in current:
				if service_key in petitioned:
					remote_service_strings.append( name + ' (-)' )

				else:
					remote_service_strings.append( name )

		return remote_service_strings

	def GetTimestamp( self, service_key ):
		if service_key in self._current_to_timestamps:
			return self._current_to_timestamps[ service_key ]

		else:
			return None

	def GetURLs( self ):
		return self._urls

	def IsDownloading( self ):
		return CC.COMBINED_LOCAL_FILE_SERVICE_KEY in self._pending

	def IsLocal( self ):
		return CC.COMBINED_LOCAL_FILE_SERVICE_KEY in self._current

	def IsRemote( self ):
		return CC.COMBINED_LOCAL_FILE_SERVICE_KEY not in self._current

	def IsTrashed( self ):
		return CC.TRASH_SERVICE_KEY in self._current

	def ProcessContentUpdate( self, service_key, content_update ):
		( data_type, action, row ) = content_update.ToTuple()

		if data_type == HC.CONTENT_TYPE_FILES:
			if action == HC.CONTENT_UPDATE_ARCHIVE:
				self._inbox = False

			elif action == HC.CONTENT_UPDATE_INBOX:
				self._inbox = True

			elif action == HC.CONTENT_UPDATE_ADD:
				self._current.add( service_key )

				self._deleted.discard( service_key )
				self._pending.discard( service_key )

				if service_key == CC.LOCAL_FILE_SERVICE_KEY:
					self._current.discard( CC.TRASH_SERVICE_KEY )
					self._pending.discard( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )

					if CC.COMBINED_LOCAL_FILE_SERVICE_KEY not in self._current:
						self._inbox = True

						self._current.add( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )

						self._current_to_timestamps[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ] = GetNow()

				self._current_to_timestamps[ service_key ] = GetNow()

			elif action == HC.CONTENT_UPDATE_DELETE:
				self._deleted.add( service_key )

				self._current.discard( service_key )
				self._petitioned.discard( service_key )

				if service_key == CC.LOCAL_FILE_SERVICE_KEY:
					self._current.add( CC.TRASH_SERVICE_KEY )

					self._current_to_timestamps[ CC.TRASH_SERVICE_KEY ] = GetNow()

				elif service_key == CC.TRASH_SERVICE_KEY:
					self._inbox = False

					self._current.discard( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )

			elif action == HC.CONTENT_UPDATE_UNDELETE:
				self._current.discard( CC.TRASH_SERVICE_KEY )

				self._deleted.discard( CC.LOCAL_FILE_SERVICE_KEY )
				self._current.add( CC.LOCAL_FILE_SERVICE_KEY )

			elif action == HC.CONTENT_UPDATE_PEND:
				if service_key not in self._current:
						self._pending.add( service_key )

			elif action == HC.CONTENT_UPDATE_PETITION:
				if service_key not in self._deleted:
						self._petitioned.add( service_key )

			elif action == HC.CONTENT_UPDATE_RESCIND_PEND:
				self._pending.discard( service_key )

			elif action == HC.CONTENT_UPDATE_RESCIND_PETITION:
				self._petitioned.discard( service_key )

		elif data_type == HC.CONTENT_TYPE_URLS:
			if action == HC.CONTENT_UPDATE_ADD:
				( urls, hashes ) = row

				self._urls.update( urls )

			elif action == HC.CONTENT_UPDATE_DELETE:
				( urls, hashes ) = row

				self._urls.difference_update( urls )

	def ResetService( self, service_key ):
		self._current.discard( service_key )
		self._pending.discard( service_key )
		self._deleted.discard( service_key )
		self._petitioned.discard( service_key )

	def ShouldIdeallyHaveThumbnail( self ):
		# file repo or local

		return len( self._current ) > 0

class Media( object ):
	def __init__( self ):

		self._id = GenerateKey()
		self._id_hash = self._id.__hash__()

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

	def __hash__( self ):
		return self._id_hash

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

class MediaList( object ):
	def __init__( self, file_service_key, media_results ):

		self._file_service_key = file_service_key

		self._hashes = set()

		self._hashes_to_singleton_media = {}
		self._hashes_to_collected_media = {}

		self._media_sort = MediaSort( ( 'system', CC.SORT_FILES_BY_FILESIZE ), CC.SORT_ASC )
		self._collect_by = []

		self._collect_map_singletons = {}
		self._collect_map_collected = {}

		self._sorted_media = SortedList( [ self._GenerateMediaSingleton( media_result ) for media_result in media_results ] )

		self._singleton_media = set( self._sorted_media )
		self._collected_media = set()

		self._RecalcHashes()

	def __len__( self ):
		return len( self._singleton_media ) + sum( map( len, self._collected_media ) )

	def _CalculateCollectionKeysToMedias( self, collect_by, medias ):
		keys_to_medias = collections.defaultdict( list )

		namespaces_to_collect_by = [ data for ( collect_by_type, data ) in collect_by if collect_by_type == 'namespace' ]
		ratings_to_collect_by = [ data for ( collect_by_type, data ) in collect_by if collect_by_type == 'rating' ]

		# services_manager = HG.client_controller.services_manager

		for media in medias:
			if len( namespaces_to_collect_by ) > 0:
				namespace_key = media.GetTagsManager().GetNamespaceSlice( namespaces_to_collect_by )

			else:
				namespace_key = None

			if len( ratings_to_collect_by ) > 0:
				rating_key = media.GetRatingsManager().GetRatingSlice( ratings_to_collect_by )

			else:
				rating_key = None

			keys_to_medias[ ( namespace_key, rating_key ) ].append( media )

		return keys_to_medias

	def _GenerateMediaCollection( self, media_results ):
		return MediaCollection( self._file_service_key, media_results )

	def _GenerateMediaSingleton( self, media_result ):
		return MediaSingleton( media_result )

	def _GetFirst( self ):
			return self._sorted_media[ 0 ]

	def _GetLast( self ):
			return self._sorted_media[ -1 ]

	def _GetMedia( self, hashes, discriminator = None ):
		if hashes.isdisjoint( self._hashes ):
			return []

		medias = []

		if discriminator is None or discriminator == 'singletons':
			medias.extend( ( self._hashes_to_singleton_media[ hash ] for hash in hashes if hash in self._hashes_to_singleton_media ) )

		if discriminator is None or discriminator == 'collections':
			medias.extend( { self._hashes_to_collected_media[ hash ] for hash in hashes if hash in self._hashes_to_collected_media } )

		return medias

	def _GetNext( self, media ):
		if media is None:
			return None

		next_index = self._sorted_media.index( media ) + 1

		if next_index == len( self._sorted_media ):
			return self._GetFirst()

		else:
			return self._sorted_media[ next_index ]

	def _GetPrevious( self, media ):
		if media is None:
				return None

		previous_index = self._sorted_media.index( media ) - 1

		if previous_index == -1:
			return self._GetLast()

		else:
			return self._sorted_media[ previous_index ]

	def _HasHashes( self, hashes ):
		for hash in hashes:
			if hash in self._hashes:
				return True

		return False

	def _RecalcHashes( self ):
		self._hashes = set()

		self._hashes_to_singleton_media = {}
		self._hashes_to_collected_media = {}

		for media in self._collected_media:
			hashes = media.GetHashes()

			self._hashes.update( hashes )

			for hash in hashes:
				self._hashes_to_collected_media[ hash ] = media

		for media in self._singleton_media:
			hash = media.GetHash()

			self._hashes.add( hash )

			self._hashes_to_singleton_media[ hash ] = media

	def _RemoveMediaByHashes( self, hashes ):
		if not isinstance( hashes, set ):
			hashes = set( hashes )

		affected_singleton_media = self._GetMedia( hashes, discriminator = 'singletons' )

		for media in self._collected_media:
			media._RemoveMediaByHashes( hashes )

		affected_collected_media = [ media for media in self._collected_media if media.HasNoMedia() ]

		self._RemoveMediaDirectly( affected_singleton_media, affected_collected_media )

	def _RemoveMediaDirectly( self, singleton_media, collected_media ):
		if not isinstance( singleton_media, set ):
			singleton_media = set( singleton_media )

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

		self._singleton_media.difference_update( singleton_media )
		self._collected_media.difference_update( collected_media )

		keys_to_remove = [ key for ( key, media ) in self._collect_map_singletons if media in singleton_media ]

		for key in keys_to_remove:
			del self._collect_map_singletons[ key ]

		keys_to_remove = [ key for ( key, media ) in self._collect_map_collected if media in collected_media ]

		for key in keys_to_remove:
			del self._collect_map_collected[ key ]

		self._sorted_media.remove_items( singleton_media.union( collected_media ) )

		self._RecalcHashes()

	def AddMedia( self, new_media, append = True ):
		if append:
			for media in new_media:
				hash = media.GetHash()

				self._hashes.add( hash )

				self._hashes_to_singleton_media[ hash ] = media

			self._singleton_media.update( new_media )
			self._sorted_media.append_items( new_media )

		else:
			if self._collect_by is not None:
				keys_to_medias = self._CalculateCollectionKeysToMedias( self._collect_by, new_media )

				new_media = []

				for ( key, medias ) in list( keys_to_medias.items() ):
					if key in self._collect_map_singletons:
						singleton_media = self._collect_map_singletons[ key ]

						self._sorted_media.remove_items( singleton_media )
						self._singleton_media.discard( singleton_media )
						del self._collect_map_singletons[ key ]

						medias.append( singleton_media )

						collected_media = self._GenerateMediaCollection( [ media.GetMediaResult() for media in medias ] )

						collected_media.Sort( self._media_sort )

						self._collected_media.add( collected_media )
						self._collect_map_collected[ key ] = collected_media

						new_media.append( collected_media )

					elif key in self._collect_map_collected:
						collected_media = self._collect_map_collected[ key ]

						self._sorted_media.remove_items( collected_media )

						collected_media.AddMedia( medias )

						collected_media.Sort( self._media_sort )

						new_media.append( collected_media )

					elif len( medias ) == 1:
						( singleton_media, ) = medias

						self._singleton_media.add( singleton_media )
						self._collect_map_singletons[ key ] = singleton_media

					else:
						collected_media = self._GenerateMediaCollection( [ media.GetMediaResult() for media in medias ] )

						collected_media.Sort( self._media_sort )

						self._collected_media.add( collected_media )
						self._collect_map_collected[ key ] = collected_media

						new_media.append( collected_media )

			self._sorted_media.insert_items( new_media )

			self._RecalcHashes()

		return new_media

	def Collect( self, collect_by = None ):
		if collect_by is None:
			collect_by = self._collect_by

		self._collect_by = collect_by

		for media in self._collected_media:
			self._singleton_media.update( [ self._GenerateMediaSingleton( media_result ) for media_result in media.GenerateMediaResults() ] )

		self._collected_media = set()

		self._collect_map_singletons = {}
		self._collect_map_collected = {}

		if len( collect_by ) > 0:
			keys_to_medias = self._CalculateCollectionKeysToMedias( collect_by, self._singleton_media )

			self._collect_map_singletons = { key : medias[0] for ( key, medias ) in list( keys_to_medias.items() ) if len( medias ) == 1 }
			self._collect_map_collected = { key : self._GenerateMediaCollection( [ media.GetMediaResult() for media in medias ] ) for ( key, medias ) in list( keys_to_medias.items() ) if len( medias ) > 1 }

			self._singleton_media = set( self._collect_map_singletons.values() )
			self._collected_media = set( self._collect_map_collected.values() )

		self._sorted_media = SortedList( list( self._singleton_media ) + list( self._collected_media ) )

		self._RecalcHashes()

	def DeletePending( self, service_key ):
		for media in self._collected_media:
			media.DeletePending( service_key )

	def GenerateMediaResults( self, has_location = None, discriminant = None, selected_media = None, unrated = None, for_media_viewer = False ):
		media_results = []

		for media in self._sorted_media:
			if has_location is not None:
				locations_manager = media.GetLocationsManager()

				if has_location not in locations_manager.GetCurrent():
					continue

			if selected_media is not None and media not in selected_media:
				continue

			if media.IsCollection():
				# don't include selected_media here as it is not valid at the deeper collection level

				media_results.extend( media.GenerateMediaResults( has_location = has_location, discriminant = discriminant, unrated = unrated, for_media_viewer = True ) )

			else:
				if discriminant is not None:
					locations_manager = media.GetLocationsManager()

					if discriminant == CC.DISCRIMINANT_INBOX:
						p = media.HasInbox()

					elif discriminant == CC.DISCRIMINANT_ARCHIVE:
						p = not media.HasInbox()

					elif discriminant == CC.DISCRIMINANT_LOCAL:
						p = locations_manager.IsLocal()

					elif discriminant == CC.DISCRIMINANT_LOCAL_BUT_NOT_IN_TRASH:
						p = locations_manager.IsLocal() and not locations_manager.IsTrashed()

					elif discriminant == CC.DISCRIMINANT_NOT_LOCAL:
						p = not locations_manager.IsLocal()

					elif discriminant == CC.DISCRIMINANT_DOWNLOADING:
						p = locations_manager.IsDownloading()

					if not p:
						continue

				if unrated is not None:
					ratings_manager = media.GetRatingsManager()

					if ratings_manager.GetRating( unrated ) is not None:
						continue

				if for_media_viewer:
					new_options = HG.client_controller.new_options

					media_show_action = new_options.GetMediaShowAction( media.GetMime() )

					if media_show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
						continue

				media_results.append( media.GetMediaResult() )

		return media_results

	def GetFirst( self ):
		return self._GetFirst()

	def GetFlatMedia( self ):
		flat_media = []

		for media in self._sorted_media:
			if media.IsCollection():
				flat_media.extend( media.GetFlatMedia() )

			else:
				flat_media.append( media )

		return flat_media

	def GetHashes( self, has_location = None, discriminant = None, not_uploaded_to = None, ordered = False ):
		if has_location is None and discriminant is None and not_uploaded_to is None and not ordered:
			return self._hashes

		else:
			if ordered:
				result = []

				for media in self._sorted_media:
					result.extend( media.GetHashes( has_location, discriminant, not_uploaded_to, ordered ) )

			else:
				result = set()

				for media in self._sorted_media:
					result.update( media.GetHashes( has_location, discriminant, not_uploaded_to, ordered ) )

			return result

	def GetLast( self ):
		return self._GetLast()

	def GetMediaIndex( self, media ):
		return self._sorted_media.index( media )

	def GetNext( self, media ):
		return self._GetNext( media )

	def GetNumFiles( self ):
		return len( self._hashes )

	def GetPrevious( self, media ):
		return self._GetPrevious( media )

	def GetSortedMedia( self ):
		return self._sorted_media

	def HasAnyOfTheseHashes( self, hashes ):
		return not hashes.isdisjoint( self._hashes )

	def HasMedia( self, media ):
		if media is None:
			return False

		if media in self._singleton_media:
			return True

		elif media in self._collected_media:
			return True

		else:
			for media_collection in self._collected_media:
				if media_collection.HasMedia( media ):
					return True

		return False

	def HasNoMedia( self ):
		return len( self._sorted_media ) == 0

	def ProcessContentUpdates( self, service_keys_to_content_updates ):
		for m in self._collected_media:
			m.ProcessContentUpdates( service_keys_to_content_updates )

		for ( service_key, content_updates ) in list( service_keys_to_content_updates.items() ):
			for content_update in content_updates:
				( data_type, action, row ) = content_update.ToTuple()

				hashes = content_update.GetHashes()

				if data_type == HC.CONTENT_TYPE_FILES:
					if action == HC.CONTENT_UPDATE_DELETE:
						local_file_domains = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )

						non_trash_local_file_services = list( local_file_domains ) + [ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ]

						local_file_services = list( non_trash_local_file_services ) + [ CC.TRASH_SERVICE_KEY ]

						deleted_from_trash_and_local_view = service_key == CC.TRASH_SERVICE_KEY and self._file_service_key in local_file_services

						trashed_and_non_trash_local_view = HC.options[ 'remove_trashed_files' ] and service_key in non_trash_local_file_services and self._file_service_key in non_trash_local_file_services

						deleted_from_repo_and_repo_view = service_key not in local_file_services and self._file_service_key == service_key

						if deleted_from_trash_and_local_view or trashed_and_non_trash_local_view or deleted_from_repo_and_repo_view:
							self._RemoveMediaByHashes( hashes )

	def ProcessServiceUpdates( self, service_keys_to_service_updates ):
		for ( service_key, service_updates ) in list( service_keys_to_service_updates.items() ):
			for service_update in service_updates:
				( action, row ) = service_update.ToTuple()

				if action == HC.SERVICE_UPDATE_DELETE_PENDING:
					self.DeletePending( service_key )

				elif action == HC.SERVICE_UPDATE_RESET:
					self.ResetService( service_key )

	def ResetService( self, service_key ):
		if service_key == self._file_service_key:
			self._RemoveMediaDirectly( self._singleton_media, self._collected_media )

		else:
			for media in self._collected_media:
				media.ResetService( service_key )

	def Sort( self, media_sort = None ):
		for media in self._collected_media:
			media.Sort( media_sort )

		if media_sort is None:
			media_sort = self._media_sort

		self._media_sort = media_sort

		media_sort_fallback = HG.client_controller.new_options.GetFallbackSort()

		( sort_key, reverse ) = media_sort_fallback.GetSortKeyAndReverse( self._file_service_key )

		self._sorted_media.sort( sort_key, reverse = reverse )

		# this is a stable sort, so the fallback order above will remain for equal items

		( sort_key, reverse ) = self._media_sort.GetSortKeyAndReverse( self._file_service_key )

		self._sorted_media.sort( sort_key = sort_key, reverse = reverse )

class ListeningMediaList( MediaList ):
	def __init__( self, file_service_key, media_results ):

		MediaList.__init__( self, file_service_key, media_results )

		HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
		HG.client_controller.sub( self, 'ProcessServiceUpdates', 'service_updates_gui' )

	def AddMediaResults( self, media_results, append = True ):
		new_media = []

		for media_result in media_results:
			hash = media_result.GetHash()

			if hash in self._hashes:
				continue

			new_media.append( self._GenerateMediaSingleton( media_result ) )

		self.AddMedia( new_media, append = append )

		return new_media

class MediaCollection( MediaList, Media ):
	def __init__( self, file_service_key, media_results ):

		Media.__init__( self )
		MediaList.__init__( self, file_service_key, media_results )

		self._archive = True
		self._inbox = False

		self._size = 0
		self._size_definite = True

		self._width = None
		self._height = None
		self._duration = None
		self._num_frames = None
		self._num_words = None
		self._tags_manager = None
		self._locations_manager = None
		self._file_viewing_stats_manager = None

		self._RecalcInternals()

	def _RecalcInternals( self ):
		self._RecalcHashes()

		self._archive = True in ( media.HasArchive() for media in self._sorted_media )
		self._inbox = True in ( media.HasInbox() for media in self._sorted_media )

		self._size = sum( [ media.GetSize() for media in self._sorted_media ] )
		self._size_definite = False not in ( media.IsSizeDefinite() for media in self._sorted_media )

		duration_sum = sum( [ media.GetDuration() for media in self._sorted_media if media.HasDuration() ] )

		if duration_sum > 0:
			self._duration = duration_sum
		else:
			self._duration = None

		tags_managers = [ m.GetTagsManager() for m in self._sorted_media ]

		self._tags_manager = MergeTagsManagers( tags_managers )

		# horrible compromise
		if len( self._sorted_media ) > 0:
			self._ratings_manager = self._sorted_media[0].GetRatingsManager()

		else:
			self._ratings_manager = RatingsManager( {} )

		all_locations_managers = [ media.GetLocationsManager() for media in self._sorted_media ]

		current = IntelligentMassIntersect( [ locations_manager.GetCurrent() for locations_manager in all_locations_managers ] )
		deleted = IntelligentMassIntersect( [ locations_manager.GetDeleted() for locations_manager in all_locations_managers ] )
		pending = IntelligentMassIntersect( [ locations_manager.GetPending() for locations_manager in all_locations_managers ] )
		petitioned = IntelligentMassIntersect( [ locations_manager.GetPetitioned() for locations_manager in all_locations_managers ] )

		self._locations_manager = LocationsManager( current, deleted, pending, petitioned )

		preview_views = 0
		preview_viewtime = 0.0
		media_views = 0
		media_viewtime = 0.0

		for m in self._sorted_media:
			fvsm = m.GetFileViewingStatsManager()

			preview_views += fvsm.preview_views
			preview_viewtime += fvsm.preview_viewtime
			media_views += fvsm.media_views
			media_viewtime += fvsm.media_viewtime

		self._file_viewing_stats_manager = FileViewingStatsManager( preview_views, preview_viewtime, media_views, media_viewtime )

	def AddMedia( self, new_media, append = True ):
		MediaList.AddMedia( self, new_media, append = True )

		self._RecalcInternals()

	def DeletePending( self, service_key ):
		MediaList.DeletePending( self, service_key )

		self._RecalcInternals()

	def GetDisplayMedia( self ):
		return self._GetFirst().GetDisplayMedia()

	def GetDuration( self ):
		return self._duration

	def GetFileViewingStatsManager( self ):
		return self._file_viewing_stats_manager

	def GetHash( self ):
		return self.GetDisplayMedia().GetHash()

	def GetLocationsManager( self ):
		return self._locations_manager

	def GetMime( self ):
		return HC.APPLICATION_HYDRUS_CLIENT_COLLECTION

	def GetNumFiles( self ):
		return len( self._hashes )

	def GetNumInbox( self ):
		return sum( ( media.GetNumInbox() for media in self._sorted_media ) )

	def GetNumFrames( self ):
		return sum( ( media.GetNumFrames() for media in self._sorted_media ) )

	def GetNumWords( self ):
		return sum( ( media.GetNumWords() for media in self._sorted_media ) )

	def GetPrettyInfoLines( self ):
		size = ToHumanBytes( self._size )

		mime = HC.mime_string_lookup[ HC.APPLICATION_HYDRUS_CLIENT_COLLECTION ]

		info_string = size + ' ' + mime

		info_string += ' (' + ToHumanInt( self.GetNumFiles() ) + ' files)'

		return [ info_string ]

	def GetRatingsManager( self ):
		return self._ratings_manager

	def GetResolution( self ):
		return ( self._width, self._height )

	def GetSingletonsTagsManagers( self ):
		tags_managers = [ m.GetTagsManager() for m in self._singleton_media ]

		for m in self._collected_media:
			tags_managers.extend( m.GetSingletonsTagsManagers() )

		return tags_managers

	def GetSize( self ):
		return self._size

	def GetTagsManager( self ):
		return self._tags_manager

	def GetTimestamp( self, service_key ):
		return None

	def HasArchive( self ):
		return self._archive

	def HasDuration( self ):
		return self._duration is not None

	def HasImages( self ):
		return True in ( media.HasImages() for media in self._sorted_media )

	def HasInbox( self ):
		return self._inbox

	def IsCollection( self ):
		return True

	def IsImage( self ):
		return False

	def IsNoisy( self ):
		return self.GetDisplayMedia().GetMime() in HC.NOISY_MIMES

	def IsSizeDefinite( self ):
		return self._size_definite

	def ProcessContentUpdates( self, service_keys_to_content_updates ):
		MediaList.ProcessContentUpdates( self, service_keys_to_content_updates )

		self._RecalcInternals()

	def RefreshFileInfo( self ):
		for media in self._sorted_media:
			media.RefreshFileInfo()

		self._RecalcInternals()

	def ResetService( self, service_key ):
		MediaList.ResetService( self, service_key )

		self._RecalcInternals()

class MediaSingleton( Media ):
	def __init__( self, media_result ):

		Media.__init__( self )

		self._media_result = media_result

	def Duplicate( self ):
		return MediaSingleton( self._media_result.Duplicate() )

	def GetDisplayMedia( self ):
		return self

	def GetDuration( self ):
		return self._media_result.GetDuration()

	def GetFileViewingStatsManager( self ):
		return self._media_result.GetFileViewingStatsManager()

	def GetHash( self ):
		return self._media_result.GetHash()

	def GetHashes( self, has_location = None, discriminant = None, not_uploaded_to = None, ordered = False ):
		if self.MatchesDiscriminant( has_location = has_location, discriminant = discriminant, not_uploaded_to = not_uploaded_to ):
			if ordered:
				return [ self._media_result.GetHash() ]

			else:
				return { self._media_result.GetHash() }

		else:
			if ordered:
				return []

			else:
				return set()

	def GetLocationsManager( self ):
		return self._media_result.GetLocationsManager()

	def GetMediaResult( self ):
		return self._media_result

	def GetMime( self ):
		return self._media_result.GetMime()

	def GetNumFiles( self ):
		return 1

	def GetNumFrames( self ):
		return self._media_result.GetNumFrames()

	def GetNumInbox( self ):
		if self.HasInbox():
			return 1
		else:
			return 0

	def GetNumWords( self ):
		return self._media_result.GetNumWords()

	def GetTimestamp( self, service_key ):
		return self._media_result.GetLocationsManager().GetTimestamp( service_key )

	def GetPrettyInfoLines( self ):
		file_info_manager = self._media_result.GetFileInfoManager()
		locations_manager = self._media_result.GetLocationsManager()

		( hash_id, hash, size, mime, width, height, duration, num_frames, num_words ) = file_info_manager.ToTuple()

		info_string = ToHumanBytes( size ) + ' ' + HC.mime_string_lookup[ mime ]

		if width is not None and height is not None:
			info_string += ' (' + ToHumanInt( width ) + 'x' + ToHumanInt( height ) + ')'

		if duration is not None:
			info_string += ', ' + ConvertMillisecondsToPrettyTime( duration )

		if num_frames is not None:
			info_string += ' (' + ToHumanInt( num_frames ) + ' frames)'

		if num_words is not None:
			info_string += ' (' + ToHumanInt( num_words ) + ' words)'

		lines = [ info_string ]

		locations_manager = self._media_result.GetLocationsManager()

		current_service_keys = locations_manager.GetCurrent()

		if CC.COMBINED_LOCAL_FILE_SERVICE_KEY in current_service_keys:
			timestamp = locations_manager.GetTimestamp( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )

			lines.append( 'imported ' + TimestampToPrettyTimeDelta( timestamp ) )

		if CC.TRASH_SERVICE_KEY in current_service_keys:
			timestamp = locations_manager.GetTimestamp( CC.TRASH_SERVICE_KEY )

			lines.append( 'trashed ' + TimestampToPrettyTimeDelta( timestamp ) )

		for service_key in current_service_keys:
			if service_key in ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, CC.LOCAL_FILE_SERVICE_KEY, CC.TRASH_SERVICE_KEY ):
				continue

			timestamp = locations_manager.GetTimestamp( service_key )

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

			service_type = service.GetServiceType()

			if service_type == HC.IPFS:
				status = 'pinned '

			else:
				status = 'uploaded '

			lines.append( status + 'to ' + service.GetName() + ' ' + TimestampToPrettyTimeDelta( timestamp ) )

		return lines

	def GetRatingsManager( self ):
		return self._media_result.GetRatingsManager()

	def GetResolution( self ):
		( width, height ) = self._media_result.GetResolution()

		if width is None:
			return ( 0, 0 )
		else:
			return ( width, height )

	def GetSize( self ):
		size = self._media_result.GetSize()

		if size is None:
			return 0
		else:
			return size

	def GetTagsManager( self ):
		return self._media_result.GetTagsManager()

	def GetTitleString( self ):
		new_options = HG.client_controller.new_options

		tag_summary_generator = new_options.GetTagSummaryGenerator( 'media_viewer_top' )

		tm = self.GetTagsManager()

		tags = tm.GetCurrent( CC.COMBINED_TAG_SERVICE_KEY ).union( tm.GetPending( CC.COMBINED_TAG_SERVICE_KEY ) )

		if len( tags ) == 0:
			return ''

		siblings_manager = HG.client_controller.tag_siblings_manager

		tags = siblings_manager.CollapseTags( CC.COMBINED_TAG_SERVICE_KEY, tags )

		summary = tag_summary_generator.GenerateSummary( tags )

		return summary

	def HasAnyOfTheseHashes( self, hashes ):
		return self._media_result.GetHash() in hashes

	def HasArchive( self ):
		return not self._media_result.GetInbox()

	def HasDuration( self ):
		# some funky formats have duration but no frames
		# some have a single 'frame' but no reported duration

		duration = self._media_result.GetDuration()

		if duration is None or duration == 0:
			return False

		num_frames = self._media_result.GetNumFrames()

		if num_frames is None or num_frames == 0:
			return False

		return True

	def HasImages( self ):
		return self.IsImage()

	def HasInbox( self ):
		return self._media_result.GetInbox()

	def IsCollection( self ):
		return False

	def IsImage( self ):
		return self._media_result.GetMime() in HC.IMAGES and not self.HasDuration()

	def IsNoisy( self ):
		return self._media_result.GetMime() in HC.NOISY_MIMES

	def IsSizeDefinite( self ):
		return self._media_result.GetSize() is not None

	def MatchesDiscriminant( self, has_location = None, discriminant = None, not_uploaded_to = None ):
		if discriminant is not None:
			inbox = self._media_result.GetInbox()

			locations_manager = self._media_result.GetLocationsManager()

			if discriminant == CC.DISCRIMINANT_INBOX:
				p = inbox

			elif discriminant == CC.DISCRIMINANT_ARCHIVE:
				p = not inbox

			elif discriminant == CC.DISCRIMINANT_LOCAL:
				p = locations_manager.IsLocal()

			elif discriminant == CC.DISCRIMINANT_LOCAL_BUT_NOT_IN_TRASH:
				p = locations_manager.IsLocal() and not locations_manager.IsTrashed()

			elif discriminant == CC.DISCRIMINANT_NOT_LOCAL:
				p = not locations_manager.IsLocal()

			elif discriminant == CC.DISCRIMINANT_DOWNLOADING:
				p = locations_manager.IsDownloading()

			if not p:
				return False

		if has_location is not None:
			locations_manager = self._media_result.GetLocationsManager()

			if has_location not in locations_manager.GetCurrent():
				return False

		if not_uploaded_to is not None:
			locations_manager = self._media_result.GetLocationsManager()

			if not_uploaded_to in locations_manager.GetCurrentRemote():
				return False

		return True

	def RefreshFileInfo( self ):
		self._media_result.RefreshFileInfo()

class MediaResult( object ):
	def __init__( self, file_info_manager, tags_manager, locations_manager, ratings_manager, file_viewing_stats_manager ):

		self._file_info_manager = file_info_manager
		self._tags_manager = tags_manager
		self._locations_manager = locations_manager
		self._ratings_manager = ratings_manager
		self._file_viewing_stats_manager = file_viewing_stats_manager

	def DeletePending( self, service_key ):
		service = HG.client_controller.services_manager.GetService( service_key )

		service_type = service.GetServiceType()

		if service_type in HC.TAG_SERVICES:
			self._tags_manager.DeletePending( service_key )

		elif service_type in HC.FILE_SERVICES:
			self._locations_manager.DeletePending( service_key )

	def Duplicate( self ):
		file_info_manager = self._file_info_manager.Duplicate()
		tags_manager = self._tags_manager.Duplicate()
		locations_manager = self._locations_manager.Duplicate()
		ratings_manager = self._ratings_manager.Duplicate()
		file_viewing_stats_manager = self._file_viewing_stats_manager.Duplicate()

		return MediaResult( file_info_manager, tags_manager, locations_manager, ratings_manager, file_viewing_stats_manager )

	def GetDuration( self ):
		return self._file_info_manager.duration

	def GetFileInfoManager( self ):
		return self._file_info_manager

	def GetFileViewingStatsManager( self ):
		return self._file_viewing_stats_manager

	def GetHash( self ):
		return self._file_info_manager.hash

	def GetHashId( self ):
		return self._file_info_manager.hash_id

	def GetInbox( self ):
		return self._locations_manager.GetInbox()

	def GetLocationsManager( self ):
		return self._locations_manager

	def GetMime( self ):
		return self._file_info_manager.mime

	def GetNumFrames( self ):
		return self._file_info_manager.num_frames

	def GetNumWords( self ):
		return self._file_info_manager.num_words

	def GetRatingsManager( self ):
		return self._ratings_manager

	def GetResolution( self ):
		return ( self._file_info_manager.width, self._file_info_manager.height )

	def GetSize( self ):
		return self._file_info_manager.size

	def GetTagsManager( self ):
		return self._tags_manager

	def ProcessContentUpdate( self, service_key, content_update ):
		try:
			service = HG.client_controller.services_manager.GetService( service_key )

		except DataMissing:
			return

		service_type = service.GetServiceType()

		if service_type in HC.TAG_SERVICES:
			self._tags_manager.ProcessContentUpdate( service_key, content_update )

		elif service_type in HC.FILE_SERVICES:
			if content_update.GetDataType() == HC.CONTENT_TYPE_FILE_VIEWING_STATS:
				self._file_viewing_stats_manager.ProcessContentUpdate( content_update )

			else:
				self._locations_manager.ProcessContentUpdate( service_key, content_update )

		elif service_type in HC.RATINGS_SERVICES:
			self._ratings_manager.ProcessContentUpdate( service_key, content_update )

	def RefreshFileInfo( self ):
		media_results = HG.client_controller.Read( 'media_results', ( self._file_info_manager.hash, ) )

		if len( media_results ) > 0:
			media_result = media_results[0]

			self._file_info_manager = media_result._file_info_manager

	def ResetService( self, service_key ):
		self._tags_manager.ResetService( service_key )
		self._locations_manager.ResetService( service_key )

	def SetTagsManager( self, tags_manager ):
		self._tags_manager = tags_manager

	def ToTuple( self ):
		return ( self._file_info_manager, self._tags_manager, self._locations_manager, self._ratings_manager )

class MediaSort( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["MEDIA_SORT"]
	SERIALISABLE_NAME = 'Media Sort'
	SERIALISABLE_VERSION = 1

	def __init__( self, sort_type = None, sort_asc = None ):
		if sort_type is None:
			sort_type = ( 'system', CC.SORT_FILES_BY_FILESIZE )

		if sort_asc is None:
			sort_asc = CC.SORT_ASC

		self.sort_type = sort_type
		self.sort_asc = sort_asc

	def _GetSerialisableInfo( self ):
		( sort_metatype, sort_data ) = self.sort_type

		if sort_metatype == 'system':
			serialisable_sort_data = sort_data

		elif sort_metatype == 'namespaces':
			serialisable_sort_data = sort_data

		elif sort_metatype == 'rating':
			service_key = sort_data

			serialisable_sort_data = service_key.hex()

		return ( sort_metatype, serialisable_sort_data, self.sort_asc )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( sort_metatype, serialisable_sort_data, self.sort_asc ) = serialisable_info

		if sort_metatype == 'system':
			sort_data = serialisable_sort_data

		elif sort_metatype == 'namespaces':
			sort_data = tuple( serialisable_sort_data )

		elif sort_metatype == 'rating':
			sort_data = bytes.fromhex( serialisable_sort_data )

		self.sort_type = ( sort_metatype, sort_data )

	def CanAsc( self ):
		( sort_metatype, sort_data ) = self.sort_type

		if sort_metatype == 'system':
			if sort_data in ( CC.SORT_FILES_BY_MIME, CC.SORT_FILES_BY_RANDOM ):
				return False

		elif sort_metatype == 'namespaces':
			return False

		return True

	def GetSortKeyAndReverse( self, file_service_key ):
		# reverse = False

		( sort_metadata, sort_data ) = self.sort_type

		def deal_with_none( x ):
			if x is None:
				return -1
			else:
				return x

		if sort_metadata == 'system':
			if sort_data == CC.SORT_FILES_BY_RANDOM:
				def sort_key( x ):
					return random.random()

			elif sort_data == CC.SORT_FILES_BY_APPROX_BITRATE:
				def sort_key( x ):

					duration = x.GetDuration()

					if duration is None or duration == 0:
						return 0

					else:
						size = x.GetSize()

						if size is None or size == 0:
							return -1

						else:
							return size / duration

			elif sort_data == CC.SORT_FILES_BY_FILESIZE:
				def sort_key( x ):

					return deal_with_none( x.GetSize() )

			elif sort_data == CC.SORT_FILES_BY_DURATION:
				def sort_key( x ):

					return deal_with_none( x.GetDuration() )

			elif sort_data == CC.SORT_FILES_BY_IMPORT_TIME:
				file_service = HG.client_controller.services_manager.GetService( file_service_key )

				file_service_type = file_service.GetServiceType()

				if file_service_type == HC.LOCAL_FILE_DOMAIN:
					file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY

				def sort_key( x ):
					return deal_with_none( x.GetTimestamp( file_service_key ) )

			elif sort_data == CC.SORT_FILES_BY_HEIGHT:
				def sort_key( x ):

					return deal_with_none( x.GetResolution()[1] )

			elif sort_data == CC.SORT_FILES_BY_WIDTH:
				def sort_key( x ):

					return deal_with_none( x.GetResolution()[0] )

			elif sort_data == CC.SORT_FILES_BY_RATIO:
				def sort_key( x ):

					( width, height ) = x.GetResolution()

					if width is None or height is None or width == 0 or height == 0:
						return -1

					else:
						return width / height

			elif sort_data == CC.SORT_FILES_BY_NUM_PIXELS:
				def sort_key( x ):

					( width, height ) = x.GetResolution()

					if width is None or height is None:
						return -1

					else:
						return width * height

			elif sort_data == CC.SORT_FILES_BY_NUM_TAGS:
				def sort_key( x ):

					tags_manager = x.GetTagsManager()

					return( len( tags_manager.GetCurrent() ) + len( tags_manager.GetPending() ) )

			elif sort_data == CC.SORT_FILES_BY_MIME:
				def sort_key( x ):

					return x.GetMime()

			elif sort_data == CC.SORT_FILES_BY_MEDIA_VIEWS:
				def sort_key( x ):

					fvsm = x.GetFileViewingStatsManager()

					return ( fvsm.media_views, fvsm.media_viewtime )

			elif sort_data == CC.SORT_FILES_BY_MEDIA_VIEWTIME:
				def sort_key( x ):
					fvsm = x.GetFileViewingStatsManager()

					return ( fvsm.media_viewtime, fvsm.media_views )

		elif sort_metadata == 'namespaces':
			namespaces = sort_data

			def sort_key( x ):
				x_tags_manager = x.GetTagsManager()

				return [ x_tags_manager.GetComparableNamespaceSlice( ( namespace, ) ) for namespace in namespaces ]

		elif sort_metadata == 'rating':
			service_key = sort_data

			def sort_key( x ):
				x_ratings_manager = x.GetRatingsManager()

				rating = deal_with_none( x_ratings_manager.GetRating( service_key ) )

				return rating

		return ( sort_key, self.sort_asc )

	def GetSortTypeString( self ):
		( sort_metatype, sort_data ) = self.sort_type

		sort_string = 'sort by '

		if sort_metatype == 'system':
			sort_string_lookup = {}

			sort_string_lookup[ CC.SORT_FILES_BY_APPROX_BITRATE ] = 'approximate bitrate'
			sort_string_lookup[ CC.SORT_FILES_BY_FILESIZE ] = 'filesize'
			sort_string_lookup[ CC.SORT_FILES_BY_DURATION ] = 'duration'
			sort_string_lookup[ CC.SORT_FILES_BY_IMPORT_TIME ] = 'time imported'
			sort_string_lookup[ CC.SORT_FILES_BY_MIME ] = 'mime'
			sort_string_lookup[ CC.SORT_FILES_BY_RANDOM ] = 'random'
			sort_string_lookup[ CC.SORT_FILES_BY_WIDTH ] = 'width'
			sort_string_lookup[ CC.SORT_FILES_BY_HEIGHT ] = 'height'
			sort_string_lookup[ CC.SORT_FILES_BY_RATIO ] = 'resolution ratio'
			sort_string_lookup[ CC.SORT_FILES_BY_NUM_PIXELS ] = 'number of pixels'
			sort_string_lookup[ CC.SORT_FILES_BY_NUM_TAGS ] = 'number of tags'
			sort_string_lookup[ CC.SORT_FILES_BY_MEDIA_VIEWS ] = 'media views'
			sort_string_lookup[ CC.SORT_FILES_BY_MEDIA_VIEWTIME ] = 'media viewtime'

			sort_string += sort_string_lookup[ sort_data ]

		elif sort_metatype == 'namespaces':
			namespaces = sort_data

			sort_string += 'tags: ' + '-'.join( namespaces )

		elif sort_metatype == 'rating':
			service_key = sort_data

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

			sort_string += 'rating:	' + service.GetName()

		return sort_string

	def GetSortAscStrings( self ):
		( sort_metatype, sort_data ) = self.sort_type

		if sort_metatype == 'system':
			sort_string_lookup = {}

			sort_string_lookup[ CC.SORT_FILES_BY_APPROX_BITRATE ] = ( 'smallest first', 'largest first', CC.SORT_DESC )
			sort_string_lookup[ CC.SORT_FILES_BY_FILESIZE ] = ( 'smallest first', 'largest first', CC.SORT_DESC )
			sort_string_lookup[ CC.SORT_FILES_BY_DURATION ] = ( 'shortest first', 'longest first', CC.SORT_DESC )
			sort_string_lookup[ CC.SORT_FILES_BY_IMPORT_TIME ] = ( 'oldest first', 'newest first', CC.SORT_DESC )
			sort_string_lookup[ CC.SORT_FILES_BY_MIME ] = ( 'mime', 'mime', CC.SORT_ASC )
			sort_string_lookup[ CC.SORT_FILES_BY_RANDOM ] = ( 'random', 'random', CC.SORT_ASC )
			sort_string_lookup[ CC.SORT_FILES_BY_WIDTH ] = ( 'slimmest first', 'widest first', CC.SORT_ASC )
			sort_string_lookup[ CC.SORT_FILES_BY_HEIGHT ] = ( 'shortest first', 'tallest first', CC.SORT_ASC )
			sort_string_lookup[ CC.SORT_FILES_BY_RATIO ] = ( 'tallest first', 'widest first', CC.SORT_ASC )
			sort_string_lookup[ CC.SORT_FILES_BY_NUM_PIXELS ] = ( 'ascending', 'descending', CC.SORT_DESC )
			sort_string_lookup[ CC.SORT_FILES_BY_NUM_TAGS ] = ( 'ascending', 'descending', CC.SORT_ASC )
			sort_string_lookup[ CC.SORT_FILES_BY_MEDIA_VIEWS ] = ( 'ascending', 'descending', CC.SORT_DESC )
			sort_string_lookup[ CC.SORT_FILES_BY_MEDIA_VIEWTIME ] = ( 'ascending', 'descending', CC.SORT_DESC )

			return sort_string_lookup[ sort_data ]

		else:
			return ( 'ascending', 'descending', CC.SORT_BY_INCIDENCE_DESC )

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["MEDIA_SORT"] ] = MediaSort

class SortedList( object ):
	def __init__( self, initial_items = None ):
		if initial_items is None:
			initial_items = []

		self._sort_key = None
		self._sort_reverse = False

		self._sorted_list = list( initial_items )

		self._items_to_indices = {}
		self._indices_dirty = True

	def __contains__( self, item ):
		if self._indices_dirty:
			self._RecalcIndices()

		return self._items_to_indices.__contains__( item )

	def __getitem__( self, value ):
		return self._sorted_list.__getitem__( value )

	def __iter__( self ):
		return iter( self._sorted_list )

	def __len__( self ):
		return len( self._sorted_list )

	def _DirtyIndices( self ):
		self._indices_dirty = True

	def _RecalcIndices( self ):
		self._items_to_indices = { item : index for ( index, item ) in enumerate( self._sorted_list ) }

		self._indices_dirty = False

	def append_items( self, items ):
		if self._indices_dirty is None:
			self._RecalcIndices()

		for ( i, item ) in enumerate( items, start = len( self._sorted_list ) ):
			self._items_to_indices[ item ] = i

		self._sorted_list.extend( items )

	def index( self, item ):
		if self._indices_dirty:
			self._RecalcIndices()

		try:
			result = self._items_to_indices[ item ]

		except KeyError:
			raise DataMissing()

		return result

	def insert_items( self, items ):
		self.append_items( items )

		self.sort()

	def remove_items( self, items ):
		deletee_indices = [ self.index( item ) for item in items ]

		deletee_indices.sort( reverse = True )

		for index in deletee_indices:
			del self._sorted_list[ index ]

		self._DirtyIndices()

	def sort( self, sort_key = None, reverse = False ):
		if sort_key is None:
			sort_key = self._sort_key
			reverse = self._sort_reverse

		else:
			self._sort_key = sort_key
			self._sort_reverse = reverse

		self._sorted_list.sort( key = sort_key, reverse = reverse )

		self._DirtyIndices()

class TagsManagerSimple( object ):
	def __init__( self, service_keys_to_statuses_to_tags ):
		self._service_keys_to_statuses_to_tags = service_keys_to_statuses_to_tags

		self._combined_namespaces_cache = None

	def _RecalcCombinedIfNeeded( self ):
		pass

	def Duplicate( self ):
		dupe_service_keys_to_statuses_to_tags = collections.defaultdict( default_dict_set )

		for ( service_key, statuses_to_tags ) in list( self._service_keys_to_statuses_to_tags.items() ):
			dupe_statuses_to_tags = default_dict_set()

			for ( status, tags ) in list( statuses_to_tags.items() ):
				dupe_statuses_to_tags[ status ] = set( tags )

			dupe_service_keys_to_statuses_to_tags[ service_key ] = dupe_statuses_to_tags

		return TagsManagerSimple( dupe_service_keys_to_statuses_to_tags )

	def GetCombinedNamespaces( self, namespaces ):
		self._RecalcCombinedIfNeeded()

		if self._combined_namespaces_cache is None:
			combined_statuses_to_tags = self._service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ]

			combined_current = combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]
			combined_pending = combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]

			pairs = ( SplitTag( tag ) for tag in combined_current.union( combined_pending ) )

			self._combined_namespaces_cache = BuildKeyToSetDict( ( namespace, subtag ) for ( namespace, subtag ) in pairs if namespace != '' )

		result = { namespace : self._combined_namespaces_cache[ namespace ] for namespace in namespaces }

		return result

	def GetComparableNamespaceSlice( self, namespaces ):
		self._RecalcCombinedIfNeeded()

		combined_statuses_to_tags = self._service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ]

		combined_current = combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]
		combined_pending = combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]

		combined = combined_current.union( combined_pending )

		pairs = [ SplitTag( tag ) for tag in combined ]

		slice = []

		for desired_namespace in namespaces:
			subtags = [ ConvertTagToSortable( subtag ) for ( namespace, subtag ) in pairs if namespace == desired_namespace ]

			subtags.sort()

			slice.append( tuple( subtags ) )

		return tuple( slice )

	def GetCurrent( self, service_key = CC.COMBINED_TAG_SERVICE_KEY ):
		if service_key == CC.COMBINED_TAG_SERVICE_KEY:
			self._RecalcCombinedIfNeeded()

		statuses_to_tags = self._service_keys_to_statuses_to_tags[ service_key ]

		return statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]

	def GetCurrentAndPending( self, service_key = CC.COMBINED_TAG_SERVICE_KEY ):
		return self.GetCurrent( service_key = service_key ).union( self.GetPending( service_key = service_key ) )

	def GetDeleted( self, service_key = CC.COMBINED_TAG_SERVICE_KEY ):
		if service_key == CC.COMBINED_TAG_SERVICE_KEY:
			self._RecalcCombinedIfNeeded()

		statuses_to_tags = self._service_keys_to_statuses_to_tags[ service_key ]

		return statuses_to_tags[ HC.CONTENT_STATUS_DELETED ]

	def GetNamespaceSlice( self, namespaces ):
		self._RecalcCombinedIfNeeded()

		combined_statuses_to_tags = self._service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ]

		combined_current = combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]
		combined_pending = combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]

		combined = combined_current.union( combined_pending )

		slice = { tag for tag in combined if True in ( tag.startswith( namespace + ':' ) for namespace in namespaces ) }

		slice = frozenset( slice )

		return slice

	def GetPending( self, service_key = CC.COMBINED_TAG_SERVICE_KEY ):
		if service_key == CC.COMBINED_TAG_SERVICE_KEY:
			self._RecalcCombinedIfNeeded()

		statuses_to_tags = self._service_keys_to_statuses_to_tags[ service_key ]

		return statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]

	def GetPetitioned( self, service_key = CC.COMBINED_TAG_SERVICE_KEY ):
		if service_key == CC.COMBINED_TAG_SERVICE_KEY:
			self._RecalcCombinedIfNeeded()

		statuses_to_tags = self._service_keys_to_statuses_to_tags[ service_key ]

		return statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ]

class TagsManager( TagsManagerSimple ):
	def __init__( self, service_keys_to_statuses_to_tags ):
		TagsManagerSimple.__init__( self, service_keys_to_statuses_to_tags )

		self._combined_is_calculated = False

	def _RecalcCombinedIfNeeded( self ):
		if not self._combined_is_calculated:
			# Combined tags are pre-collapsed by siblings

			siblings_manager = HG.client_controller.tag_siblings_manager

			combined_statuses_to_tags = collections.defaultdict( set )

			for ( service_key, statuses_to_tags ) in list( self._service_keys_to_statuses_to_tags.items() ):
				if service_key == CC.COMBINED_TAG_SERVICE_KEY:
					continue

				statuses_to_tags = siblings_manager.CollapseStatusesToTags( service_key, statuses_to_tags )

				combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ].update( statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ] )
				combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].update( statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] )
				combined_statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ].update( statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ] )
				combined_statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].update( statuses_to_tags[ HC.CONTENT_STATUS_DELETED ] )

			self._service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ] = combined_statuses_to_tags

			self._combined_namespaces_cache = None

			self._combined_is_calculated = True

	def DeletePending( self, service_key ):
		statuses_to_tags = self._service_keys_to_statuses_to_tags[ service_key ]

		if len( statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] ) + len( statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ] ) > 0:
			statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] = set()
			statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ] = set()

			self._combined_is_calculated = False

	def Duplicate( self ):
		dupe_service_keys_to_statuses_to_tags = collections.defaultdict( default_dict_set )

		for ( service_key, statuses_to_tags ) in list( self._service_keys_to_statuses_to_tags.items() ):
			dupe_statuses_to_tags = default_dict_set()

			for ( status, tags ) in list( statuses_to_tags.items() ):
				dupe_statuses_to_tags[ status ] = set( tags )

			dupe_service_keys_to_statuses_to_tags[ service_key ] = dupe_statuses_to_tags

		return TagsManager( dupe_service_keys_to_statuses_to_tags )

	def GetNumTags( self, service_key, include_current_tags = True, include_pending_tags = False ):
		if service_key == CC.COMBINED_TAG_SERVICE_KEY:
			self._RecalcCombinedIfNeeded()

		num_tags = 0

		statuses_to_tags = self.GetStatusesToTags( service_key )

		if include_current_tags:
			num_tags += len( statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ] )
		if include_pending_tags:
			num_tags += len( statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] )

		return num_tags

	def GetServiceKeysToStatusesToTags( self ):
		self._RecalcCombinedIfNeeded()

		return self._service_keys_to_statuses_to_tags

	def GetStatusesToTags( self, service_key ):
		if service_key == CC.COMBINED_TAG_SERVICE_KEY:
			self._RecalcCombinedIfNeeded()

		return self._service_keys_to_statuses_to_tags[ service_key ]

	def HasTag( self, tag ):
		self._RecalcCombinedIfNeeded()

		combined_statuses_to_tags = self._service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ]

		return tag in combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ] or tag in combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]

	def NewSiblings( self ):
		self._combined_is_calculated = False

	def ProcessContentUpdate( self, service_key, content_update ):
		statuses_to_tags = self._service_keys_to_statuses_to_tags[ service_key ]

		( data_type, action, row ) = content_update.ToTuple()

		( tag, hashes ) = row

		if action == HC.CONTENT_UPDATE_ADD:
			statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ].add( tag )

			statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].discard( tag )
			statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].discard( tag )

		elif action == HC.CONTENT_UPDATE_DELETE:
			statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].add( tag )

			statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ].discard( tag )
			statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ].discard( tag )

		elif action == HC.CONTENT_UPDATE_PEND:
			if tag not in statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]:
				statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].add( tag )

		elif action == HC.CONTENT_UPDATE_RESCIND_PEND:
			statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].discard( tag )

		elif action == HC.CONTENT_UPDATE_PETITION:
			if tag in statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]:
				statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ].add( tag )

		elif action == HC.CONTENT_UPDATE_RESCIND_PETITION:
			statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ].discard( tag )

		self._combined_is_calculated = False

	def ResetService( self, service_key ):
		if service_key in self._service_keys_to_statuses_to_tags:
			del self._service_keys_to_statuses_to_tags[ service_key ]

			self._combined_is_calculated = False