code:
import os, threading, wx

from lib.Hydrus.Defaults import GetDefaultCheckerOptions
from lib.Hydrus.Duplicates import DuplicateActionOptions
from lib.Hydrus.Data import GenerateKey
from lib.Hydrus.Media import MediaSort
from lib.Hydrus.Serialisable import SerialisableDictionary, SerialisableList, \
	SerialisableBase, CreateFromSerialisableTuple
from lib.Hydrus.Tags import CleanTags
from lib.Hydrus.Tags.Local import TagFilter

### CONSTANTS ###

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

### CLASSES ###

class ClientOptions( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["CLIENT_OPTIONS"]
	SERIALISABLE_NAME = 'Client Options'
	SERIALISABLE_VERSION = 3

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

		self._dictionary = SerialisableDictionary()

		self._lock = threading.Lock()

		self._InitialiseDefaults()

	def _GetSerialisableInfo( self ):
		serialisable_info = self._dictionary.GetSerialisableTuple()

		return serialisable_info

	def _InitialiseDefaults( self ):
		self._dictionary[ 'booleans' ] = {}

		self._dictionary[ 'booleans' ][ 'advanced_mode' ] = False

		self._dictionary[ 'booleans' ][ 'always_show_hover_windows' ] = False

		self._dictionary[ 'booleans' ][ 'apply_all_parents_to_all_services' ] = False
		self._dictionary[ 'booleans' ][ 'apply_all_siblings_to_all_services' ] = False
		self._dictionary[ 'booleans' ][ 'filter_inbox_and_archive_predicates' ] = False

		self._dictionary[ 'booleans' ][ 'discord_dnd_fix' ] = False
		self._dictionary[ 'booleans' ][ 'secret_discord_dnd_fix' ] = False

		self._dictionary[ 'booleans' ][ 'disable_cv_for_gifs' ] = False

		self._dictionary[ 'booleans' ][ 'set_search_focus_on_page_change' ] = False

		self._dictionary[ 'booleans' ][ 'allow_remove_on_manage_tags_input' ] = True
		self._dictionary[ 'booleans' ][ 'add_parents_on_manage_tags' ] = True
		self._dictionary[ 'booleans' ][ 'replace_siblings_on_manage_tags' ] = True
		self._dictionary[ 'booleans' ][ 'yes_no_on_remove_on_manage_tags' ] = True

		self._dictionary[ 'booleans' ][ 'show_related_tags' ] = False
		self._dictionary[ 'booleans' ][ 'show_file_lookup_script_tags' ] = False
		self._dictionary[ 'booleans' ][ 'hide_message_manager_on_gui_iconise' ] = HC.PLATFORM_OSX
		self._dictionary[ 'booleans' ][ 'hide_message_manager_on_gui_deactive' ] = False

		self._dictionary[ 'booleans' ][ 'load_images_with_pil' ] = False

		self._dictionary[ 'booleans' ][ 'use_system_ffmpeg' ] = False

		self._dictionary[ 'booleans' ][ 'maintain_similar_files_duplicate_pairs_during_idle' ] = False

		self._dictionary[ 'booleans' ][ 'show_namespaces' ] = True

		self._dictionary[ 'booleans' ][ 'verify_regular_https' ] = True

		self._dictionary[ 'booleans' ][ 'reverse_page_shift_drag_behaviour' ] = False

		self._dictionary[ 'booleans' ][ 'anchor_and_hide_canvas_drags' ] = HC.PLATFORM_WINDOWS

		self._dictionary[ 'booleans' ][ 'thumbnail_fill' ] = False

		self._dictionary[ 'booleans' ][ 'import_page_progress_display' ] = True

		self._dictionary[ 'booleans' ][ 'process_subs_in_random_order' ] = True

		self._dictionary[ 'booleans' ][ 'ac_select_first_with_count' ] = False

		self._dictionary[ 'booleans' ][ 'saving_sash_positions_on_exit' ] = True

		self._dictionary[ 'booleans' ][ 'pause_all_new_network_traffic' ] = False
		self._dictionary[ 'booleans' ][ 'pause_all_file_queues' ] = False
		self._dictionary[ 'booleans' ][ 'pause_all_watcher_checkers' ] = False
		self._dictionary[ 'booleans' ][ 'pause_all_gallery_searches' ] = False

		self._dictionary[ 'booleans' ][ 'notebook_tabs_on_left' ] = False

		self._dictionary[ 'booleans' ][ 'popup_message_force_min_width' ] = False

		self._dictionary[ 'booleans' ][ 'always_show_iso_time' ] = False

		self._dictionary[ 'booleans' ][ 'use_advanced_file_deletion_dialog' ] = False

		self._dictionary[ 'booleans' ][ 'show_new_on_file_seed_short_summary' ] = False
		self._dictionary[ 'booleans' ][ 'show_deleted_on_file_seed_short_summary' ] = False

		self._dictionary[ 'booleans' ][ 'only_save_last_session_during_idle' ] = False

		self._dictionary[ 'booleans' ][ 'do_human_sort_on_hdd_file_import_paths' ] = True

		self._dictionary[ 'booleans' ][ 'highlight_new_watcher' ] = True
		self._dictionary[ 'booleans' ][ 'highlight_new_query' ] = True

		self._dictionary[ 'booleans' ][ 'delete_files_after_export' ] = False

		self._dictionary[ 'booleans' ][ 'file_viewing_statistics_active' ] = True

		self._dictionary[ 'booleans' ][ 'file_system_waits_on_wakeup' ] = False

		self._dictionary[ 'booleans' ][ 'always_show_system_everything' ] = False

		#

		self._dictionary[ 'colours' ] = SerialisableDictionary()

		self._dictionary[ 'colours' ][ 'default' ] = SerialisableDictionary()

		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMB_BACKGROUND ] = ( 255, 255, 255 )
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMB_BACKGROUND_SELECTED ] = ( 217, 242, 255 ) # light blue
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMB_BACKGROUND_REMOTE ] = ( 32, 32, 36 ) # 50% Payne's Gray
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED ] = ( 64, 64, 72 ) # Payne's Gray
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMB_BORDER ] = ( 223, 227, 230 ) # light grey
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMB_BORDER_SELECTED ] = ( 1, 17, 26 ) # dark grey
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMB_BORDER_REMOTE ] = ( 248, 208, 204 ) # 25% Vermillion, 75% White
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED ] = ( 227, 66, 52 ) # Vermillion, lol
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_THUMBGRID_BACKGROUND ] = ( 255, 255, 255 )
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_AUTOCOMPLETE_BACKGROUND ] = ( 235, 248, 255 ) # very light blue
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_MEDIA_BACKGROUND ] = ( 255, 255, 255 )
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_MEDIA_TEXT ] = ( 0, 0, 0 )
		self._dictionary[ 'colours' ][ 'default' ][ CC.COLOUR_TAGS_BOX ] = ( 255, 255, 255 )

		self._dictionary[ 'colours' ][ 'darkmode' ] = SerialisableDictionary()

		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMB_BACKGROUND ] = ( 64, 64, 72 ) # Payne's Gray
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMB_BACKGROUND_SELECTED ] = ( 112, 128, 144 ) # Slate Gray
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMB_BACKGROUND_REMOTE ] = ( 64, 13, 2 ) # Black Bean
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED ] = ( 171, 39, 79 ) # Amaranth Purple
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMB_BORDER ] = ( 145, 163, 176 ) # Cadet Grey
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMB_BORDER_SELECTED ] = ( 223, 227, 230 ) # light grey
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMB_BORDER_REMOTE ] = ( 248, 208, 204 ) # 25% Vermillion, 75% White
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED ] = ( 227, 66, 52 ) # Vermillion, lol
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_THUMBGRID_BACKGROUND ] = ( 52, 52, 52 ) # 20% flat gray
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_AUTOCOMPLETE_BACKGROUND ] = ( 83, 98, 103 ) # Gunmetal
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_MEDIA_BACKGROUND ] = ( 52, 52, 52 ) # 20% flat gray
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_MEDIA_TEXT ] = ( 112, 128, 144 ) # Slate Gray
		self._dictionary[ 'colours' ][ 'darkmode' ][ CC.COLOUR_TAGS_BOX ] = ( 0, 0, 0 )

		#

		self._dictionary[ 'duplicate_action_options' ] = SerialisableDictionary()
		self._dictionary[ 'duplicate_action_options' ][ HC.DUPLICATE_BETTER ] = DuplicateActionOptions( [ ( CC.LOCAL_TAG_SERVICE_KEY, HC.CONTENT_MERGE_ACTION_MOVE, TagFilter() ) ], [], True, True, sync_urls_action = HC.CONTENT_MERGE_ACTION_COPY )
		self._dictionary[ 'duplicate_action_options' ][ HC.DUPLICATE_SAME_QUALITY ] = DuplicateActionOptions( [ ( CC.LOCAL_TAG_SERVICE_KEY, HC.CONTENT_MERGE_ACTION_TWO_WAY_MERGE, TagFilter() ) ], [], False, True, sync_urls_action = HC.CONTENT_MERGE_ACTION_TWO_WAY_MERGE )
		self._dictionary[ 'duplicate_action_options' ][ HC.DUPLICATE_ALTERNATE ] = DuplicateActionOptions( [], [], False )
		self._dictionary[ 'duplicate_action_options' ][ HC.DUPLICATE_NOT_DUPLICATE ] = DuplicateActionOptions( [], [], False )

		#

		self._dictionary[ 'integers' ] = {}

		self._dictionary[ 'integers' ][ 'video_buffer_size_mb' ] = 96

		self._dictionary[ 'integers' ][ 'related_tags_search_1_duration_ms' ] = 250
		self._dictionary[ 'integers' ][ 'related_tags_search_2_duration_ms' ] = 2000
		self._dictionary[ 'integers' ][ 'related_tags_search_3_duration_ms' ] = 6000

		self._dictionary[ 'integers' ][ 'suggested_tags_width' ] = 300

		self._dictionary[ 'integers' ][ 'similar_files_duplicate_pairs_search_distance' ] = 0

		self._dictionary[ 'integers' ][ 'default_new_page_goes' ] = CC.NEW_PAGE_GOES_FAR_RIGHT

		self._dictionary[ 'integers' ][ 'max_page_name_chars' ] = 20
		self._dictionary[ 'integers' ][ 'page_file_count_display' ] = CC.PAGE_FILE_COUNT_DISPLAY_ALL

		self._dictionary[ 'integers' ][ 'network_timeout' ] = 10

		self._dictionary[ 'integers' ][ 'thumbnail_visibility_scroll_percent' ] = 75

		self._dictionary[ 'integers' ][ 'total_pages_warning' ] = 165

		self._dictionary[ 'integers' ][ 'last_session_save_period_minutes' ] = 5

		self._dictionary[ 'integers' ][ 'shutdown_work_period' ] = 86400

		self._dictionary[ 'integers' ][ 'max_network_jobs' ] = 15
		self._dictionary[ 'integers' ][ 'max_network_jobs_per_domain' ] = 3

		self._dictionary[ 'integers' ][ 'max_simultaneous_subscriptions' ] = 1

		self._dictionary[ 'integers' ][ 'gallery_page_wait_period_pages' ] = 15
		self._dictionary[ 'integers' ][ 'gallery_page_wait_period_subscriptions' ] = 5
		self._dictionary[ 'integers' ][ 'watcher_page_wait_period' ] = 5

		self._dictionary[ 'integers' ][ 'popup_message_character_width' ] = 56

		self._dictionary[ 'integers' ][ 'video_thumbnail_percentage_in' ] = 35

		self._dictionary[ 'integers' ][ 'duplicate_comparison_score_higher_filesize' ] = 20
		self._dictionary[ 'integers' ][ 'duplicate_comparison_score_much_higher_filesize' ] = 10
		self._dictionary[ 'integers' ][ 'duplicate_comparison_score_higher_resolution' ] = 20
		self._dictionary[ 'integers' ][ 'duplicate_comparison_score_much_higher_resolution' ] = 5
		self._dictionary[ 'integers' ][ 'duplicate_comparison_score_more_tags' ] = 8
		self._dictionary[ 'integers' ][ 'duplicate_comparison_score_older' ] = 5

		self._dictionary[ 'integers' ][ 'thumbnail_cache_timeout' ] = 86400
		self._dictionary[ 'integers' ][ 'image_cache_timeout' ] = 600

		self._dictionary[ 'integers' ][ 'thumbnail_border' ] = 1
		self._dictionary[ 'integers' ][ 'thumbnail_margin' ] = 2

		self._dictionary[ 'integers' ][ 'subscription_network_error_delay' ] = 12 * 3600
		self._dictionary[ 'integers' ][ 'subscription_other_error_delay' ] = 36 * 3600
		self._dictionary[ 'integers' ][ 'downloader_network_error_delay' ] = 90 * 60

		self._dictionary[ 'integers' ][ 'file_viewing_stats_menu_display' ] = CC.FILE_VIEWING_STATS_MENU_DISPLAY_MEDIA_AND_PREVIEW_IN_SUBMENU

		self._dictionary[ 'integers' ][ 'number_of_gui_session_backups' ] = 10

		#

		self._dictionary[ 'keys' ] = {}

		self._dictionary[ 'keys' ][ 'default_tag_service_search_page' ] = CC.COMBINED_TAG_SERVICE_KEY.hex()
		self._dictionary[ 'keys' ][ 'default_gug_key' ] = GenerateKey().hex()

		self._dictionary[ 'key_list' ] = {}

		self._dictionary[ 'key_list' ][ 'default_neighbouring_txt_tag_service_keys' ] = []

		#

		self._dictionary[ 'noneable_integers' ] = {}

		self._dictionary[ 'noneable_integers' ][ 'forced_search_limit' ] = None

		self._dictionary[ 'noneable_integers' ][ 'disk_cache_maintenance_mb' ] = 256
		self._dictionary[ 'noneable_integers' ][ 'disk_cache_init_period' ] = 4

		self._dictionary[ 'noneable_integers' ][ 'num_recent_tags' ] = 20

		self._dictionary[ 'noneable_integers' ][ 'maintenance_vacuum_period_days' ] = 30

		self._dictionary[ 'noneable_integers' ][ 'duplicate_background_switch_intensity' ] = 3

		self._dictionary[ 'noneable_integers' ][ 'last_review_bandwidth_search_distance' ] = 7 * 86400

		#

		self._dictionary[ 'simple_downloader_formulae' ] = SerialisableList()

		#

		self._dictionary[ 'noneable_strings' ] = {}

		self._dictionary[ 'noneable_strings' ][ 'favourite_file_lookup_script' ] = 'gelbooru md5'
		self._dictionary[ 'noneable_strings' ][ 'suggested_tags_layout' ] = 'notebook'
		self._dictionary[ 'noneable_strings' ][ 'backup_path' ] = None
		self._dictionary[ 'noneable_strings' ][ 'web_browser_path' ] = None
		self._dictionary[ 'noneable_strings' ][ 'last_png_export_dir' ] = None
		self._dictionary[ 'noneable_strings' ][ 'media_background_bmp_path' ] = None
		self._dictionary[ 'noneable_strings' ][ 'http_proxy' ] = None
		self._dictionary[ 'noneable_strings' ][ 'https_proxy' ] = None

		self._dictionary[ 'strings' ] = {}

		self._dictionary[ 'strings' ][ 'main_gui_title' ] = 'hydrus client'
		self._dictionary[ 'strings' ][ 'namespace_connector' ] = ':'
		self._dictionary[ 'strings' ][ 'export_phrase' ] = '{hash}'
		self._dictionary[ 'strings' ][ 'current_colourset' ] = 'default'
		self._dictionary[ 'strings' ][ 'favourite_simple_downloader_formula' ] = 'all files linked by images in page'
		self._dictionary[ 'strings' ][ 'thumbnail_scroll_rate' ] = '1.0'
		self._dictionary[ 'strings' ][ 'pause_character' ] = '\u23F8'
		self._dictionary[ 'strings' ][ 'stop_character' ] = '\u23F9'
		self._dictionary[ 'strings' ][ 'default_gug_name' ] = 'safebooru tag search'

		self._dictionary[ 'string_list' ] = {}

		self._dictionary[ 'string_list' ][ 'default_media_viewer_custom_shortcuts' ] = []
		self._dictionary[ 'string_list' ][ 'favourite_tags' ] = []
		self._dictionary[ 'string_list' ][ 'advanced_file_deletion_reasons' ] = [ 'I do not like it.', 'It is bad quality.', 'It is not appropriate for this client.', 'Temporary delete--I want to bring it back later.' ]

		#

		self._dictionary[ 'tag_summary_generators' ] = SerialisableDictionary()

		namespace_info = []

		namespace_info.append( ( 'creator', '', ', ' ) )
		namespace_info.append( ( 'series', '', ', ' ) )
		namespace_info.append( ( 'title', '', ', ' ) )

		separator = ' - '

		# the cleantags here converts to unicode, which is important!

		example_tags = CleanTags( [ 'creator:creator', 'series:series', 'title:title' ] )

		import lib.GUI.Tags

		tsg = lib.GUI.Tags.TagSummaryGenerator( namespace_info = namespace_info, separator = separator, example_tags = example_tags )

		self._dictionary[ 'tag_summary_generators' ][ 'thumbnail_top' ] = tsg

		namespace_info = []

		namespace_info.append( ( 'volume', 'v', '-' ) )
		namespace_info.append( ( 'chapter', 'c', '-' ) )
		namespace_info.append( ( 'page', 'p', '-' ) )

		separator = '-'

		example_tags = CleanTags( [ 'volume:3', 'chapter:10', 'page:330', 'page:331' ] )

		tsg = lib.GUI.Tags.TagSummaryGenerator( namespace_info = namespace_info, separator = separator, example_tags = example_tags )

		self._dictionary[ 'tag_summary_generators' ][ 'thumbnail_bottom_right' ] = tsg

		namespace_info = []

		namespace_info.append( ( 'creator', '', ', ' ) )
		namespace_info.append( ( 'series', '', ', ' ) )
		namespace_info.append( ( 'title', '', ', ' ) )
		namespace_info.append( ( 'volume', 'v', '-' ) )
		namespace_info.append( ( 'chapter', 'c', '-' ) )
		namespace_info.append( ( 'page', 'p', '-' ) )

		separator = ' - '

		example_tags = CleanTags( [ 'creator:creator', 'series:series', 'title:title', 'volume:1', 'chapter:1', 'page:1' ] )

		tsg = lib.GUI.Tags.TagSummaryGenerator( namespace_info = namespace_info, separator = separator, example_tags = example_tags )

		self._dictionary[ 'tag_summary_generators' ][ 'media_viewer_top' ] = tsg

		#

		self._dictionary[ 'default_file_import_options' ] = SerialisableDictionary()

		exclude_deleted = True
		do_not_check_known_urls_before_importing = False
		do_not_check_hashes_before_importing = False
		allow_decompression_bombs = False
		min_size = None
		max_size = None
		max_gif_size = 32 * 1048576
		min_resolution = None
		max_resolution = None

		automatic_archive = False
		associate_source_urls = True

		present_new_files = True
		present_already_in_inbox_files = False
		present_already_in_archive_files = False

		from lib.Actions.Import.Options import FileImportOptions

		quiet_file_import_options = FileImportOptions()

		quiet_file_import_options.SetPreImportOptions( exclude_deleted, do_not_check_known_urls_before_importing, do_not_check_hashes_before_importing, allow_decompression_bombs, min_size, max_size, max_gif_size, min_resolution, max_resolution )
		quiet_file_import_options.SetPostImportOptions( automatic_archive, associate_source_urls )
		quiet_file_import_options.SetPresentationOptions( present_new_files, present_already_in_inbox_files, present_already_in_archive_files )

		self._dictionary[ 'default_file_import_options' ][ 'quiet' ] = quiet_file_import_options

		present_new_files = True
		present_already_in_inbox_files = True
		present_already_in_archive_files = True

		loud_file_import_options = lib.Actions.Import.Options.FileImportOptions()

		loud_file_import_options.SetPreImportOptions( exclude_deleted, do_not_check_known_urls_before_importing, do_not_check_hashes_before_importing, allow_decompression_bombs, min_size, max_size, max_gif_size, min_resolution, max_resolution )
		loud_file_import_options.SetPostImportOptions( automatic_archive, associate_source_urls )
		loud_file_import_options.SetPresentationOptions( present_new_files, present_already_in_inbox_files, present_already_in_archive_files )

		self._dictionary[ 'default_file_import_options' ][ 'loud' ] = loud_file_import_options

		#

		self._dictionary[ 'frame_locations' ] = {}

		# remember size, remember position, last_size, last_pos, default gravity, default position, maximised, fullscreen
		self._dictionary[ 'frame_locations' ][ 'file_import_status' ] = ( True, True, None, None, ( -1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'gallery_import_log' ] = ( True, True, None, None, ( -1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'local_import_filename_tagging' ] = ( True, False, None, None, ( -1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'main_gui' ] = ( True, True, ( 800, 600 ), ( 20, 20 ), ( -1, -1 ), 'topleft', True, False )
		self._dictionary[ 'frame_locations' ][ 'manage_options_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'manage_subscriptions_dialog' ] = ( True, True, None, None, ( 1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'edit_subscription_dialog' ] = ( True, True, None, None, ( 1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'manage_tags_dialog' ] = ( False, False, None, None, ( -1, 1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'manage_tags_frame' ] = ( False, False, None, None, ( -1, 1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'media_viewer' ] = ( True, True, ( 640, 480 ), ( 70, 70 ), ( -1, -1 ), 'topleft', True, True )
		self._dictionary[ 'frame_locations' ][ 'regular_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'review_services' ] = ( False, True, None, None, ( -1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'deeply_nested_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
		self._dictionary[ 'frame_locations' ][ 'regular_center_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'center', False, False )

		#

		self._dictionary[ 'media_launch' ] = SerialisableDictionary() # integer keys, so got to be cleverer dict

		for mime in HC.SEARCHABLE_MIMES:
			self._dictionary[ 'media_launch' ][ mime ] = None

		#

		self._dictionary[ 'media_view' ] = SerialisableDictionary() # integer keys, so got to be cleverer dict

		# media_show_action, preview_show_action, ( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) )

		image_zoom_info = ( CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, False, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
		gif_zoom_info = ( CC.MEDIA_VIEWER_SCALE_MAX_REGULAR, CC.MEDIA_VIEWER_SCALE_MAX_REGULAR, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, True, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
		flash_zoom_info = ( CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, False, CC.ZOOM_LINEAR, CC.ZOOM_LINEAR )
		video_zoom_info = ( CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_MAX_REGULAR, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, True, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
		null_zoom_info = ( CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, False, CC.ZOOM_LINEAR, CC.ZOOM_LINEAR )

		self._dictionary[ 'media_view' ][ HC.IMAGE_JPEG ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, image_zoom_info )
		self._dictionary[ 'media_view' ][ HC.IMAGE_PNG ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, image_zoom_info )
		self._dictionary[ 'media_view' ][ HC.IMAGE_APNG ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, gif_zoom_info )
		self._dictionary[ 'media_view' ][ HC.IMAGE_GIF ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, gif_zoom_info )
		self._dictionary[ 'media_view' ][ HC.IMAGE_WEBP ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, image_zoom_info )
		self._dictionary[ 'media_view' ][ HC.IMAGE_TIFF ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, image_zoom_info )

		if HC.PLATFORM_WINDOWS:
			self._dictionary[ 'media_view' ][ HC.APPLICATION_FLASH ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_BEHIND_EMBED, CC.MEDIA_VIEWER_ACTION_SHOW_BEHIND_EMBED, flash_zoom_info )

		else:
			self._dictionary[ 'media_view' ][ HC.APPLICATION_FLASH ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )

		self._dictionary[ 'media_view' ][ HC.APPLICATION_PDF ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.APPLICATION_PSD ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.APPLICATION_ZIP ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.APPLICATION_7Z ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.APPLICATION_RAR ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.APPLICATION_HYDRUS_UPDATE_CONTENT ] = ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.APPLICATION_HYDRUS_UPDATE_DEFINITIONS ] = ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, null_zoom_info )

		self._dictionary[ 'media_view' ][ HC.VIDEO_AVI ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, video_zoom_info )
		self._dictionary[ 'media_view' ][ HC.VIDEO_FLV ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, video_zoom_info )
		self._dictionary[ 'media_view' ][ HC.VIDEO_MOV ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, video_zoom_info )
		self._dictionary[ 'media_view' ][ HC.VIDEO_MP4 ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, video_zoom_info )
		self._dictionary[ 'media_view' ][ HC.VIDEO_MPEG ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, video_zoom_info )
		self._dictionary[ 'media_view' ][ HC.VIDEO_MKV ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, video_zoom_info )
		self._dictionary[ 'media_view' ][ HC.VIDEO_WEBM ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, video_zoom_info )
		self._dictionary[ 'media_view' ][ HC.VIDEO_WMV ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL, video_zoom_info )
		self._dictionary[ 'media_view' ][ HC.AUDIO_MP3 ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.AUDIO_OGG ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.AUDIO_FLAC ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )
		self._dictionary[ 'media_view' ][ HC.AUDIO_WMA ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, null_zoom_info )

		self._dictionary[ 'media_zooms' ] = [ 0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0, 3.0, 5.0, 10.0, 20.0 ]

		#

		self._dictionary[ 'misc' ] = SerialisableDictionary()

		self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ] = GetDefaultCheckerOptions( 'thread' )
		self._dictionary[ 'misc' ][ 'default_subscription_checker_options' ] = GetDefaultCheckerOptions( 'artist subscription' )

		#

		self._dictionary[ 'suggested_tags' ] = SerialisableDictionary()

		self._dictionary[ 'suggested_tags' ][ 'favourites' ] = {}

		#

		import lib.Hydrus.Media

		self._dictionary[ 'default_sort' ] = MediaSort( ( 'system', CC.SORT_FILES_BY_FILESIZE ), CC.SORT_ASC )
		self._dictionary[ 'fallback_sort' ] = MediaSort( ( 'system', CC.SORT_FILES_BY_IMPORT_TIME ), CC.SORT_ASC )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		loaded_dictionary = CreateFromSerialisableTuple( serialisable_info )

		for ( key, value ) in list( loaded_dictionary.items() ):
			if key in self._dictionary and isinstance( self._dictionary[ key ], dict ) and isinstance( value, dict ):
				self._dictionary[ key ].update( value )

			else:
				self._dictionary[ key ] = value

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

			if 'media_view' in loaded_dictionary:
				mimes = list( loaded_dictionary[ 'media_view' ].keys() )

				for mime in mimes:
					if mime in self._dictionary[ 'media_view' ]:
						( default_media_show_action, default_preview_show_action, default_zoom_info ) = self._dictionary[ 'media_view' ][ mime ]

						( media_show_action, preview_show_action, zoom_in_to_fit, exact_zooms_only, scale_up_quality, scale_down_quality ) = loaded_dictionary[ 'media_view' ][ mime ]

						loaded_dictionary[ 'media_view' ][ mime ] = ( media_show_action, preview_show_action, default_zoom_info )

					else:
						# while devving this, I discovered some u'20' stringified keys had snuck in and hung around. let's nuke them here, in case anyone else got similar

						del loaded_dictionary[ 'media_view' ][ mime ]

			new_serialisable_info = loaded_dictionary.GetSerialisableTuple()

			return ( 2, new_serialisable_info )

		if version == 2:
			# as db_dir is now moveable, let's move portable base from base_dir to db_dir

			def update_portable_path( p ):
				if p is None:
					return p

				p = os.path.normpath( p ) # collapses .. stuff and converts / to \\ for windows only

				if os.path.isabs( p ):
					a_p = p

				else:
					a_p = os.path.normpath( os.path.join( HC.BASE_DIR, p ) )

				if not HC.PLATFORM_WINDOWS and not os.path.exists( a_p ):
					a_p = a_p.replace( '\\', '/' )

				try:
					db_dir = HG.controller.GetDBDir()

					p = os.path.relpath( a_p, db_dir )

					if p.startswith( '..' ):
						p = a_p

				except Exception:
					p = a_p

				if HC.PLATFORM_WINDOWS:
					p = p.replace( '\\', '/' ) # store seps as /, to maintain multiplatform uniformity

				return p

			loaded_dictionary = CreateFromSerialisableTuple( old_serialisable_info )

			if 'client_files_locations_ideal_weights' in loaded_dictionary:
				updated_cfliw = []

				for ( old_portable_path, weight ) in loaded_dictionary[ 'client_files_locations_ideal_weights' ]:
					new_portable_path = update_portable_path( old_portable_path )

					updated_cfliw.append( ( new_portable_path, weight ) )

				loaded_dictionary[ 'client_files_locations_ideal_weights' ] = updated_cfliw

			if 'client_files_locations_resized_thumbnail_override' in loaded_dictionary:
				loaded_dictionary[ 'client_files_locations_resized_thumbnail_override' ] = update_portable_path( loaded_dictionary[ 'client_files_locations_resized_thumbnail_override' ] )

			if 'client_files_locations_full_size_thumbnail_override' in loaded_dictionary:
				loaded_dictionary[ 'client_files_locations_full_size_thumbnail_override' ] = update_portable_path( loaded_dictionary[ 'client_files_locations_full_size_thumbnail_override' ] )

			new_serialisable_info = loaded_dictionary.GetSerialisableTuple()

			return ( 3, new_serialisable_info )

	def FlipBoolean( self, name ):
		with self._lock:
			self._dictionary[ 'booleans' ][ name ] = not self._dictionary[ 'booleans' ][ name ]

	def GetBoolean( self, name ):
		with self._lock:
			return self._dictionary[ 'booleans' ][ name ]

	def GetColour( self, colour_type, colourset = None ):
		with self._lock:
			if colourset is None:
				colourset = self._dictionary[ 'strings' ][ 'current_colourset' ]

			( r, g, b ) = self._dictionary[ 'colours' ][ colourset ][ colour_type ]

			return wx.Colour( r, g, b )

	def GetDefaultFileImportOptions( self, options_type ):
		with self._lock:
			return self._dictionary[ 'default_file_import_options' ][ options_type ]

	def GetDefaultWatcherCheckerOptions( self ):
		with self._lock:
			return self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ]

	def GetDefaultSort( self ):
		with self._lock:
			return self._dictionary[ 'default_sort' ]

	def GetDefaultSubscriptionCheckerOptions( self ):
		with self._lock:
			return self._dictionary[ 'misc' ][ 'default_subscription_checker_options' ]

	def GetDuplicateActionOptions( self, duplicate_type ):
		with self._lock:
			return self._dictionary[ 'duplicate_action_options' ][ duplicate_type ]

	def GetFallbackSort( self ):
		with self._lock:
			return self._dictionary[ 'fallback_sort' ]

	def GetFrameLocation( self, frame_key ):
		with self._lock:
			return self._dictionary[ 'frame_locations' ][ frame_key ]

	def GetFrameLocations( self ):
		with self._lock:
			return list( self._dictionary[ 'frame_locations' ].items() )

	def GetInteger( self, name ):
		with self._lock:
			return self._dictionary[ 'integers' ][ name ]

	def GetKey( self, name ):
		with self._lock:
			return bytes.fromhex( self._dictionary[ 'keys' ][ name ] )

	def GetKeyList( self, name ):
		with self._lock:
			return [ bytes.fromhex( hex_key ) for hex_key in self._dictionary[ 'key_list' ][ name ] ]

	def GetMediaShowAction( self, mime ):
		with self._lock:
			( media_show_action, preview_show_action, zoom_info ) = self._dictionary[ 'media_view' ][ mime ]

			if media_show_action not in CC.media_viewer_capabilities[ mime ]:
				return CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON

			return media_show_action

	def GetMediaViewOptions( self, mime ):
		with self._lock:
			return self._dictionary[ 'media_view' ][ mime ]

	def GetMediaZoomOptions( self, mime ):
		with self._lock:
			( media_show_action, preview_show_action, zoom_info ) = self._dictionary[ 'media_view' ][ mime ]

			return zoom_info

	def GetMediaZooms( self ):
		with self._lock:
			return list( self._dictionary[ 'media_zooms' ] )

	def GetMediaZoomQuality( self, mime ):
		with self._lock:
			( media_show_action, preview_show_action, ( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) ) = self._dictionary[ 'media_view' ][ mime ]

			return ( scale_up_quality, scale_down_quality )

	def GetMimeLaunch( self, mime ):
		with self._lock:
			if mime not in self._dictionary[ 'media_launch' ]:
				self._dictionary[ 'media_launch' ][ mime ] = None

			return self._dictionary[ 'media_launch' ][ mime ]

	def GetNoneableInteger( self, name ):
		with self._lock:
			return self._dictionary[ 'noneable_integers' ][ name ]

	def GetNoneableString( self, name ):
		with self._lock:
			return self._dictionary[ 'noneable_strings' ][ name ]

	def GetPreviewShowAction( self, mime ):
		with self._lock:
			( media_show_action, preview_show_action, zoom_info ) = self._dictionary[ 'media_view' ][ mime ]

			if preview_show_action not in CC.media_viewer_capabilities[ mime ]:
				return CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON

			return preview_show_action

	def GetSimpleDownloaderFormulae( self ):
		with self._lock:
			return self._dictionary[ 'simple_downloader_formulae' ]

	def GetString( self, name ):
		with self._lock:
			return self._dictionary[ 'strings' ][ name ]

	def GetStringList( self, name ):
		with self._lock:
			return self._dictionary[ 'string_list' ][ name ]

	def GetSuggestedTagsFavourites( self, service_key ):
		with self._lock:
			service_key_hex = service_key.hex()

			stf = self._dictionary[ 'suggested_tags' ][ 'favourites' ]

			if service_key_hex in stf:
				return set( stf[ service_key_hex ] )

			else:
				return set()

	def GetTagSummaryGenerator( self, name ):
		with self._lock:
			return self._dictionary[ 'tag_summary_generators' ][ name ]

	def InvertBoolean( self, name ):
		with self._lock:
			self._dictionary[ 'booleans' ][ name ] = not self._dictionary[ 'booleans' ][ name ]

	def SetBoolean( self, name, value ):
		with self._lock:
			self._dictionary[ 'booleans' ][ name ] = value

	def SetColour( self, colour_type, colourset, colour ):
		with self._lock:
			if isinstance( colour, wx.Colour ):
				( r, g, b, a ) = colour.Get()

			else:
				( r, g, b ) = colour

			self._dictionary[ 'colours' ][ colourset ][ colour_type ] = ( r, g, b )

	def SetDefaultWatcherCheckerOptions( self, checker_options ):
		with self._lock:
			self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ] = checker_options

	def SetDefaultSort( self, media_sort ):
		with self._lock:
			self._dictionary[ 'default_sort' ] = media_sort

	def SetDefaultSubscriptionCheckerOptions( self, checker_options ):
		with self._lock:
			self._dictionary[ 'misc' ][ 'default_subscription_checker_options' ] = checker_options

	def SetDuplicateActionOptions( self, duplicate_type, duplicate_action_options ):
		with self._lock:
			self._dictionary[ 'duplicate_action_options' ][ duplicate_type ] = duplicate_action_options

	def SetFallbackSort( self, media_sort ):
		with self._lock:
			self._dictionary[ 'fallback_sort' ] = media_sort

	def SetDefaultFileImportOptions( self, options_type, file_import_options ):
		with self._lock:
			self._dictionary[ 'default_file_import_options' ][ options_type ] = file_import_options

	def SetFrameLocation( self, frame_key, remember_size, remember_position, last_size, last_position, default_gravity, default_position, maximised, fullscreen ):
		with self._lock:
			self._dictionary[ 'frame_locations' ][ frame_key ] = ( remember_size, remember_position, last_size, last_position, default_gravity, default_position, maximised, fullscreen )

	def SetInteger( self, name, value ):
		with self._lock:
			self._dictionary[ 'integers' ][ name ] = value

	def SetKey( self, name, value ):
		with self._lock:
			self._dictionary[ 'keys' ][ name ] = value.hex()

	def SetKeyList( self, name, value ):
		with self._lock:
			self._dictionary[ 'key_list' ][ name ] = [ key.hex() for key in value ]

	def SetMediaViewOptions( self, mime, value_tuple ):
		with self._lock:
			self._dictionary[ 'media_view' ][ mime ] = value_tuple

	def SetMediaZooms( self, zooms ):
		with self._lock:
			self._dictionary[ 'media_zooms' ] = zooms

	def SetMimeLaunch( self, mime, launch_path ):
		with self._lock:
			self._dictionary[ 'media_launch' ][ mime ] = launch_path

	def SetNoneableInteger( self, name, value ):
		with self._lock:
			self._dictionary[ 'noneable_integers' ][ name ] = value

	def SetNoneableString( self, name, value ):
		with self._lock:
			self._dictionary[ 'noneable_strings' ][ name ] = value

	def SetSimpleDownloaderFormulae( self, simple_downloader_formulae ):
		with self._lock:
			self._dictionary[ 'simple_downloader_formulae' ] = SerialisableList( simple_downloader_formulae )

	def SetString( self, name, value ):
		with self._lock:
			# it_changed = False

			if value is not None and value != '':
				if self._dictionary[ 'strings' ][ name ] != value:
					self._dictionary[ 'strings' ][ name ] = value

					# it_changed = True

	def SetStringList( self, name, value ):
		with self._lock:
			self._dictionary[ 'string_list' ][ name ] = list( value )

	def SetSuggestedTagsFavourites( self, service_key, tags ):
		with self._lock:
			service_key_hex = service_key.hex()

			self._dictionary[ 'suggested_tags' ][ 'favourites' ][ service_key_hex ] = list( tags )

	def SetTagSummaryGenerator( self, name, tag_summary_generator ):
		with self._lock:
			self._dictionary[ 'tag_summary_generators' ][ name ] = tag_summary_generator

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["CLIENT_OPTIONS"] ] = ClientOptions


code:
import webbrowser

from lib.Hydrus.Paths import RecyclePath, DeletePath, LaunchFile

### CONSTANTS ###

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

### FUNCTIONS ###

def LocalDeletePath( path, always_delete_fully = False ):
	if HC.options[ 'delete_to_recycle_bin' ] is True and not always_delete_fully:
		RecyclePath( path )

	else:
		DeletePath( path )

def LaunchPathInWebBrowser( path ):
	LaunchURLInWebBrowser( 'file:///' + path )

def LaunchURLInWebBrowser( url ):
	web_browser_path = HG.client_controller.new_options.GetNoneableString( 'web_browser_path' )

	if web_browser_path is None:
		webbrowser.open( url )

	else:
		LaunchFile( url, launch_path = web_browser_path )

license: wtfpl


code:
import lib.Hydrus.Constants as HC
import lib.Hydrus.Globals as HG

from lib.Hydrus.Data import BigJobPauser, DebugPrint, GetNow, GetSubprocessKWArgs, Print, ShowException, \
	ShowText, TimeHasPassed, ToHumanBytes
from lib.Hydrus.Threading import CheckIfThreadShuttingDown

import gc, os, psutil, re, send2trash, shlex, shutil, stat, subprocess, tempfile, threading, traceback

### CONSTANTS ###

TEMP_PATH_LOCK = threading.Lock()
IN_USE_TEMP_PATHS = set()

### FUNCTIONS ###

def AppendPathUntilNoConflicts( path ):
	( path_absent_ext, ext ) = os.path.splitext( path )

	good_path_absent_ext = path_absent_ext

	i = 0

	while os.path.exists( good_path_absent_ext + ext ):
		good_path_absent_ext = path_absent_ext + '_' + str( i )

		i += 1

	return good_path_absent_ext + ext

def CleanUpTempPath( os_file_handle, temp_path ):
	try:
		os.close( os_file_handle )

	except OSError:
		gc.collect()

		try:
			os.close( os_file_handle )

		except OSError:
			Print( 'Could not close the temporary file ' + temp_path )

			return

	try:
		os.remove( temp_path )

	except OSError:
		with TEMP_PATH_LOCK:
			IN_USE_TEMP_PATHS.add( ( GetNow(), temp_path ) )

def CleanUpOldTempPaths():
	with TEMP_PATH_LOCK:
		data = list( IN_USE_TEMP_PATHS )

		for row in data:
			( time_failed, temp_path ) = row

			if TimeHasPassed( time_failed + 60 ):
				try:
					os.remove( temp_path )

					IN_USE_TEMP_PATHS.discard( row )

				except OSError:
					if TimeHasPassed( time_failed + 600 ):
						IN_USE_TEMP_PATHS.discard( row )

def ConvertAbsPathToPortablePath( abs_path, base_dir_override = None ):
	try:
		if base_dir_override is None:
			base_dir = HG.controller.GetDBDir()

		else:
			base_dir = base_dir_override

		portable_path = os.path.relpath( abs_path, base_dir )

		if portable_path.startswith( '..' ):
			portable_path = abs_path

	except Exception:
		portable_path = abs_path

	if HC.PLATFORM_WINDOWS:
		portable_path = portable_path.replace( '\\', '/' ) # store seps as /, to maintain multiplatform uniformity

	return portable_path

def ConvertPortablePathToAbsPath( portable_path, base_dir_override = None ):
	portable_path = os.path.normpath( portable_path ) # collapses .. stuff and converts / to \\ for windows only

	if os.path.isabs( portable_path ):
		abs_path = portable_path

	else:
		if base_dir_override is None:
			base_dir = HG.controller.GetDBDir()

		else:
			base_dir = base_dir_override

		abs_path = os.path.normpath( os.path.join( base_dir, portable_path ) )

	if not HC.PLATFORM_WINDOWS and not os.path.exists( abs_path ):
		abs_path = abs_path.replace( '\\', '/' )

	return abs_path

def CopyAndMergeTree( source, dest ):
	pauser = BigJobPauser()

	MakeSureDirectoryExists( dest )

	num_errors = 0

	for ( root, dirnames, filenames ) in os.walk( source ):
		dest_root = root.replace( source, dest )

		for dirname in dirnames:
			pauser.Pause()

			source_path = os.path.join( root, dirname )
			dest_path = os.path.join( dest_root, dirname )

			MakeSureDirectoryExists( dest_path )

			shutil.copystat( source_path, dest_path )

		for filename in filenames:
			if num_errors > 5:
				raise Exception( 'Too many errors, directory copy abandoned.' )

			pauser.Pause()

			source_path = os.path.join( root, filename )
			dest_path = os.path.join( dest_root, filename )

			ok = MirrorFile( source_path, dest_path )

			if not ok:
				num_errors += 1

def CopyFileLikeToFileLike( f_source, f_dest ):
	for block in ReadFileLikeAsBlocks( f_source ):
		f_dest.write( block )

def DeletePath( path ):
	if os.path.exists( path ):
		MakeFileWritable( path )

		try:
			if os.path.isdir( path ):
				shutil.rmtree( path )

			else:
				os.remove( path )

		except Exception as e:
			if 'Error 32' in str( e ):
				# file in use by another process

				DebugPrint( 'Trying to delete ' + path + ' failed because it was in use by another process.' )

			else:
				ShowText( 'Trying to delete ' + path + ' caused the following error:' )
				ShowException( e )

def DirectoryIsWritable( path ):
	if not os.path.exists( path ):
		return False

	try:
		t = tempfile.TemporaryFile( dir = path )

		t.close()

		return True

	except Exception:
		return False

def FilterFreePaths( paths ):
	free_paths = []

	for path in paths:
		CheckIfThreadShuttingDown()

		if PathIsFree( path ):
			free_paths.append( path )

	return free_paths

def GetCurrentTempDir():
	return tempfile.gettempdir()

def GetDefaultLaunchPath():
	if HC.PLATFORM_WINDOWS:
		return 'windows is called directly'

	elif HC.PLATFORM_OSX:
		return 'open "%path%"'

	elif HC.PLATFORM_LINUX:
		return 'xdg-open "%path%"'

def GetDevice( path ):
	path = path.lower()

	partition_infos = psutil.disk_partitions( all = True )

	def sort_descending_mountpoint( partition_info ): # i.e. put '/home' before '/'

		return - len( partition_info.mountpoint )

	partition_infos.sort( key = sort_descending_mountpoint )

	for partition_info in partition_infos:
		if path.startswith( partition_info.mountpoint.lower() ):
			return partition_info.device

	return None

def GetFreeSpace( path ):
	disk_usage = psutil.disk_usage( path )

	return disk_usage.free

def GetTempDir( dir = None ):
	return tempfile.mkdtemp( prefix = 'hydrus', dir = dir )

def GetTempPath( suffix = '', dir = None ):
	return tempfile.mkstemp( suffix = suffix, prefix = 'hydrus', dir = dir )

def HasSpaceForDBTransaction( db_dir, num_bytes ):
	if HG.no_db_temp_files:
		space_needed = int( num_bytes * 1.1 )

		approx_available_memory = psutil.virtual_memory().available * 4 / 5

		if approx_available_memory < num_bytes:
			return ( False, 'I believe you need about ' + ToHumanBytes( space_needed ) + ' available memory, since you are running in no_db_temp_files mode, but you only seem to have ' + ToHumanBytes( approx_available_memory ) + '.' )

		db_disk_free_space = GetFreeSpace( db_dir )

		if db_disk_free_space < space_needed:
			return ( False, 'I believe you need about ' + ToHumanBytes( space_needed ) + ' on your db\'s partition, but you only seem to have ' + ToHumanBytes( db_disk_free_space ) + '.' )

	else:
		temp_dir = tempfile.gettempdir()

		temp_disk_free_space = GetFreeSpace( temp_dir )

		temp_and_db_on_same_device = GetDevice( temp_dir ) == GetDevice( db_dir )

		if temp_and_db_on_same_device:
			space_needed = int( num_bytes * 2.2 )

			if temp_disk_free_space < space_needed:
				return ( False, 'I believe you need about ' + ToHumanBytes( space_needed ) + ' on your db\'s partition, which I think also holds your temporary path, but you only seem to have ' + ToHumanBytes( temp_disk_free_space ) + '.' )

		else:
			space_needed = int( num_bytes * 1.1 )

			if temp_disk_free_space < space_needed:
				return ( False, 'I believe you need about ' + ToHumanBytes( space_needed ) + ' on your temporary path\'s partition, which I think is ' + temp_dir + ', but you only seem to have ' + ToHumanBytes( temp_disk_free_space ) + '.' )

			db_disk_free_space = GetFreeSpace( db_dir )

			if db_disk_free_space < space_needed:
				return ( False, 'I believe you need about ' + ToHumanBytes( space_needed ) + ' on your db\'s partition, but you only seem to have ' + ToHumanBytes( db_disk_free_space ) + '.' )

	return ( True, 'You seem to have enough space!' )

def LaunchDirectory( path ):
	def do_it():
		if HC.PLATFORM_WINDOWS:
			os.startfile( path )

		else:
			if HC.PLATFORM_OSX:
				cmd = [ 'open', path ]

			elif HC.PLATFORM_LINUX:
				cmd = [ 'xdg-open', path ]

			# setsid call un-childs this new process

			sbp_kwargs = GetSubprocessKWArgs()

			process = subprocess.Popen( cmd, preexec_fn = os.setsid, **sbp_kwargs )

			process.communicate()

	thread = threading.Thread( target = do_it )

	thread.daemon = True

	thread.start()

def LaunchFile( path, launch_path = None ):
	def do_it( launch_path ):
		if HC.PLATFORM_WINDOWS and launch_path is None:
			os.startfile( path )

		else:
			if launch_path is None:
				launch_path = GetDefaultLaunchPath()

			complete_launch_path = launch_path.replace( '%path%', path )

			if HC.PLATFORM_WINDOWS:
				cmd = complete_launch_path
				preexec_fn = None

			else:
				# setsid call un-childs this new process

				cmd = shlex.split( complete_launch_path )

				preexec_fn = os.setsid

			if HG.subprocess_report_mode:
				message = 'Attempting to launch ' + path + ' using command ' + repr( cmd ) + '.'

				ShowText( message )

			try:
				sbp_kwargs = GetSubprocessKWArgs( text = True )

				process = subprocess.Popen( cmd, preexec_fn = preexec_fn, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, **sbp_kwargs )

				( stdout, stderr ) = process.communicate()

				if HG.subprocess_report_mode:
					if stdout is None and stderr is None:
						ShowText( 'No stdout or stderr came back.' )

					if stdout is not None:
						ShowText( 'stdout: ' + repr( stdout ) )

					if stderr is not None:
						ShowText( 'stderr: ' + repr( stderr ) )

			except Exception as e:
				ShowText( 'Could not launch a file! Command used was:' + os.linesep + str( cmd ) )

				ShowException( e )

	thread = threading.Thread( target = do_it, args = ( launch_path, ) )

	thread.daemon = True

	thread.start()

def MakeSureDirectoryExists( path ):
	if not os.path.exists( path ):
		os.makedirs( path )

def MakeFileWritable( path, recursive = True ):
	if not os.path.exists( path ):
		return

	try:
		stat_result = os.stat( path )

		current_bits = stat_result.st_mode

		if HC.PLATFORM_WINDOWS:
			# this is actually the same value as S_IWUSR, but let's not try to second guess ourselves
			desired_bit = stat.S_IWRITE

		else:
			desired_bit = stat.S_IWUSR

		if not desired_bit & current_bits:
			os.chmod( path, current_bits | desired_bit )

		if os.path.isdir( path ) and recursive:
			for ( root, dirnames, filenames ) in os.walk( path ):
				for dirname in dirnames:
					sub_path = os.path.join( root, dirname )

					MakeFileWritable( sub_path, recursive = False )

				for filename in filenames:
					sub_path = os.path.join( root, filename )

					MakeFileWritable( sub_path )

	except Exception as e:
		Print( 'Wanted to add write permission to "{}", but had an error: {}'.format( path, str( e ) ) )

def MergeFile( source, dest ):
	if not os.path.isdir( source ):
		MakeFileWritable( source )

	if PathsHaveSameSizeAndDate( source, dest ):
		DeletePath( source )

	else:
		try:
			# this overwrites on conflict without hassle
			shutil.move( source, dest )

		except Exception as e:
			ShowText( 'Trying to move ' + source + ' to ' + dest + ' caused the following problem:' )

			ShowException( e )

			return False

	return True

def MergeTree( source, dest, text_update_hook = None ):
	pauser = BigJobPauser()

	if not os.path.exists( dest ):
		try:
			shutil.move( source, dest )

		except OSError:
			# if there were read only files in source and this was partition to partition, the copy2 goes ok but the subsequent source unlink fails
			# so, if it seems this has happened, let's just try a walking mergetree, which should be able to deal with these readonlies on a file-by-file basis
			if os.path.exists( dest ):
				MergeTree( source, dest, text_update_hook = text_update_hook )

	else:
		if len( os.listdir( dest ) ) == 0:
			for filename in os.listdir( source ):
				source_path = os.path.join( source, filename )
				dest_path = os.path.join( dest, filename )

				if not os.path.isdir( source_path ):
					MakeFileWritable( source_path )

				shutil.move( source_path, dest_path )

		else:
			num_errors = 0

			for ( root, dirnames, filenames ) in os.walk( source ):
				if text_update_hook is not None:
					text_update_hook( 'Copying ' + root + '.' )

				dest_root = root.replace( source, dest )

				for dirname in dirnames:
					pauser.Pause()

					source_path = os.path.join( root, dirname )
					dest_path = os.path.join( dest_root, dirname )

					MakeSureDirectoryExists( dest_path )

					shutil.copystat( source_path, dest_path )

				for filename in filenames:
					if num_errors > 5:
						raise Exception( 'Too many errors, directory move abandoned.' )

					pauser.Pause()

					source_path = os.path.join( root, filename )
					dest_path = os.path.join( dest_root, filename )

					ok = MergeFile( source_path, dest_path )

					if not ok:
						num_errors += 1

			if num_errors == 0:
				DeletePath( source )

def MirrorFile( source, dest ):
	if not PathsHaveSameSizeAndDate( source, dest ):
		try:
			MakeFileWritable( dest )

			# this overwrites on conflict without hassle
			shutil.copy2( source, dest )

		except Exception as e:
			ShowText( 'Trying to copy ' + source + ' to ' + dest + ' caused the following problem:' )

			ShowException( e )

			return False

	return True

def MirrorTree( source, dest, text_update_hook = None, is_cancelled_hook = None ):
	pauser = BigJobPauser()

	MakeSureDirectoryExists( dest )

	num_errors = 0

	for ( root, dirnames, filenames ) in os.walk( source ):
		if is_cancelled_hook is not None and is_cancelled_hook():
			return

		if text_update_hook is not None:
			text_update_hook( 'Copying ' + root + '.' )

		dest_root = root.replace( source, dest )

		surplus_dest_paths = { os.path.join( dest_root, dest_filename ) for dest_filename in os.listdir( dest_root ) }

		for dirname in dirnames:
			pauser.Pause()

			source_path = os.path.join( root, dirname )
			dest_path = os.path.join( dest_root, dirname )

			surplus_dest_paths.discard( dest_path )

			MakeSureDirectoryExists( dest_path )

			shutil.copystat( source_path, dest_path )

		for filename in filenames:
			if num_errors > 5:
				raise Exception( 'Too many errors, directory copy abandoned.' )

			pauser.Pause()

			source_path = os.path.join( root, filename )

			dest_path = os.path.join( dest_root, filename )

			surplus_dest_paths.discard( dest_path )

			ok = MirrorFile( source_path, dest_path )

			if not ok:
				num_errors += 1

		for dest_path in surplus_dest_paths:
			pauser.Pause()

			DeletePath( dest_path )

def OpenFileLocation( path ):
	def do_it():
		if HC.PLATFORM_WINDOWS:
			cmd = [ 'explorer', '/select,', path ]

		elif HC.PLATFORM_OSX:
			cmd = [ 'open', '-R', path ]

		elif HC.PLATFORM_LINUX:
			raise NotImplementedError( 'Linux cannot open file locations!' )

		sbp_kwargs = GetSubprocessKWArgs( hide_terminal = False )

		process = subprocess.Popen( cmd, **sbp_kwargs )

		process.communicate()

	thread = threading.Thread( target = do_it )

	thread.daemon = True

	thread.start()

def PathsHaveSameSizeAndDate( path1, path2 ):
	if os.path.exists( path1 ) and os.path.exists( path2 ):
		same_size = os.path.getsize( path1 ) == os.path.getsize( path2 )
		same_modified_time = int( os.path.getmtime( path1 ) ) == int( os.path.getmtime( path2 ) )

		if same_size and same_modified_time:
			return True

	return False

def PathIsFree( path ):
	try:
		os.rename( path, path ) # rename a path to itself

		return True

	except OSError: # 'already in use by another process' or an odd filename too long error

		Print( 'Already in use/inaccessible: ' + path )

	return False

def ReadFileLikeAsBlocks( f ):
	next_block = f.read( HC.READ_BLOCK_SIZE )

	while len( next_block ) > 0:
		yield next_block

		next_block = f.read( HC.READ_BLOCK_SIZE )

def RecyclePath( path ):
	if os.path.exists( path ):
		MakeFileWritable( path )

		try:
			send2trash.send2trash( path )

		except Exception:
			Print( 'Trying to recycle ' + path + ' created this error:' )

			DebugPrint( traceback.format_exc() )

			Print( 'It has been fully deleted instead.' )

			DeletePath( path )

def SanitizeFilename( filename ):
	if HC.PLATFORM_WINDOWS:
		# \, /, :, *, ?, ", <, >, |
		filename = re.sub( r'\\|/|:|\*|\?|"|<|>|\|', '_', filename )

	else:
		filename = re.sub( '/', '_', filename )

	return filename


code:
from .Paths import *

__all__ = [
	"Local"
]

from . import *


code:
import os
import sqlite3

HASH_TYPE_MD5 = 0 # 16 bytes long
HASH_TYPE_SHA1 = 1 # 20 bytes long
HASH_TYPE_SHA256 = 2 # 32 bytes long
HASH_TYPE_SHA512 = 3 # 64 bytes long

# Please feel free to use this file however you wish.
# None of this is thread-safe, though, so don't try to do anything clever.

# A rating for hydrus is a float from 0.0 to 1.0
# dislike/like are 0.0 and 1.0
# numerical are fractions between 0.0 and 1.0
# for a four-star rating that allows 0 stars, the 5 possibles are:
 	0.0, 0.25, 0.5, 0.75, 1.0
# for a three-star rating that does not allow 0 stars, the three possibles are:
 	0.0, 0.5, 1.0
# in truth, at our level:
# 	a five-star rating that does allow stars is a six-star rating
# 	a ten-star rating that does not allow stars is a ten-star rating

# If you want to make a new rating archive for use in hydrus, you want to do something like:
# import HydrusRatingArchive
# hra = lib.Hydrus.Ratings.Archive.HydrusRatingArchive( 'my_little_archive.db' )
# hra.SetHashType( lib.Hydrus.Ratings.Archive.HASH_TYPE_MD5 )
# hra.SetNumberOfStars( 5 )
# hra.BeginBigJob()
# for ( hash, rating ) in my_rating_generator:
 	hra.AddRating( hash, rating )
# hra.CommitBigJob()
# del hra

# If you are only adding a couple ratings, you can exclude the BigJob stuff. It just makes millions of sequential writes more efficient.

# Also, this manages hashes as bytes, not hex, so if you have something like:
# hash = ab156e87c5d6e215ab156e87c5d6e215

# Then go hash = bytes.fromhex( hash ) before you pass it to Add/Get/Has/SetRating

# And also feel free to contact me directly at hydrus.admin@gmail.com if you need help.

### CLASSES ###

class HydrusRatingArchive( object ):
	def __init__( self, path ):
		self._path = path

		if not os.path.exists( self._path ):
			create_db = True
		else:
			create_db = False

		self._InitDBCursor()

		if create_db:
			self._InitDB()

	def _InitDB( self ):
		self._c.execute( 'CREATE TABLE hash_type ( hash_type INTEGER );', )

		self._c.execute( 'CREATE TABLE number_of_stars ( number_of_stars INTEGER );', )

		self._c.execute( 'CREATE TABLE ratings ( hash BLOB PRIMARY KEY, rating REAL );' )

	def _InitDBCursor( self ):
		self._db = sqlite3.connect( self._path, isolation_level = None, detect_types = sqlite3.PARSE_DECLTYPES )

		self._c = self._db.cursor()

	def BeginBigJob( self ):
		self._c.execute( 'BEGIN IMMEDIATE;' )

	def CommitBigJob( self ):
		self._c.execute( 'COMMIT;' )
		self._c.execute( 'VACUUM;' )

	def DeleteRating( self, hash ):
		self._c.execute( 'DELETE FROM ratings WHERE hash = ?;', ( sqlite3.Binary( hash ), ) )

	def GetHashType( self ):
		result = self._c.execute( 'SELECT hash_type FROM hash_type;' ).fetchone()

		if result is None:
			result = self._c.execute( 'SELECT hash FROM hashes;' ).fetchone()

			if result is None:
				raise Exception( 'This archive has no hash type set, and as it has no files, no hash type guess can be made.' )

			if len( hash ) == 16:
				hash_type = HASH_TYPE_MD5
			elif len( hash ) == 20:
				hash_type = HASH_TYPE_SHA1
			elif len( hash ) == 32:
				hash_type = HASH_TYPE_SHA256
			elif len( hash ) == 64:
				hash_type = HASH_TYPE_SHA512
			else:
				raise Exception( 'This archive has non-standard hashes. Something is wrong.' )

			self.SetHashType( hash_type )

			return hash_type

		else:
			( hash_type, ) = result

			return hash_type

	def GetName( self ):
		filename = os.path.basename( self._path )

		if '.' in filename:
			filename = filename.split( '.', 1 )[0]

		return filename

	def GetNumberOfStars( self ):
		result = self._c.execute( 'SELECT number_of_stars FROM number_of_stars;' ).fetchone()

		if result is None:
			raise Exception( 'This rating archive has no number of stars set.' )

		else:
			( number_of_stars, ) = result

			return number_of_stars

	def GetRating( self, hash ):
		result = self._c.execute( 'SELECT rating FROM ratings WHERE hash = ?;', ( sqlite3.Binary( hash ), ) ).fetchone()

		if result is None:
			return None

		else:
			( rating, ) = result

			return rating

	def HasHash( self, hash ):
		result = self._c.execute( 'SELECT 1 FROM ratings WHERE hash = ?;', ( sqlite3.Binary( hash ), ) ).fetchone()

		if result is None:
			return False

		else:
			return True

	def IterateRatings( self ):
		for row in self._c.execute( 'SELECT hash, rating FROM ratings;' ):
			yield row

	def SetHashType( self, hash_type ):
		self._c.execute( 'DELETE FROM hash_type;' )

		self._c.execute( 'INSERT INTO hash_type ( hash_type ) VALUES ( ? );', ( hash_type, ) )

	def SetNumberOfStars( self, number_of_stars ):
		self._c.execute( 'DELETE FROM number_of_stars;' )

		self._c.execute( 'INSERT INTO number_of_stars ( number_of_stars ) VALUES ( ? );', ( number_of_stars, ) )

	def SetRating( self, hash, rating ):
		self._c.execute( 'REPLACE INTO ratings ( hash, rating ) VALUES ( ?, ? );', ( sqlite3.Binary( hash ), rating ) )


code:
import wx

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

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

### FUNCTIONS ###

def DrawLike( dc, x, y, service_key, rating_state ):
	shape = GetShape( service_key )

	( pen_colour, brush_colour ) = GetPenAndBrushColours( service_key, rating_state )

	dc.SetPen( wx.Pen( pen_colour ) )
	dc.SetBrush( wx.Brush( brush_colour ) )

	if shape == CIRCLE:
		dc.DrawCircle( x + 7, y + 7, 6 )

	elif shape == SQUARE:
		dc.DrawRectangle( x + 2, y + 2, 12, 12 )

	elif shape == STAR:
		dc.DrawPolygon( STAR_COORDS, x + 1, y + 1 )

def DrawNumerical( dc, x, y, service_key, rating_state, rating ):
	( shape, stars ) = GetStars( service_key, rating_state, rating )

	x_delta = 0
	x_step = 12

	for ( num_stars, pen_colour, brush_colour ) in stars:
		dc.SetPen( wx.Pen( pen_colour ) )
		dc.SetBrush( wx.Brush( brush_colour ) )

		for i in range( num_stars ):
			if shape == CIRCLE:
				dc.DrawCircle( x + 7 + x_delta, y + 7, 6 )

			elif shape == SQUARE:
				dc.DrawRectangle( x + 2 + x_delta, y + 2, 12, 12 )

			elif shape == STAR:
				dc.DrawPolygon( STAR_COORDS, x + 1 + x_delta, y + 1 )

			x_delta += x_step

def GetLikeStateFromMedia( media, service_key ):
	on_exists = False
	off_exists = False
	null_exists = False

	for m in media:
		ratings_manager = m.GetRatingsManager()

		rating = ratings_manager.GetRating( service_key )

		if rating == 1:
			on_exists = True

		elif rating == 0:
			off_exists = True

		elif rating is None:
			null_exists = True

	if len( [ b for b in ( on_exists, off_exists, null_exists ) if b ] ) == 1:
		if on_exists:
			return LIKE
		elif off_exists:
			return DISLIKE
		else:
			return NULL

	else:
		return MIXED

def GetLikeStateFromRating( rating ):
	if rating == 1:
		return LIKE
	elif rating == 0:
		return DISLIKE
	else:
		return NULL

def GetNumericalStateFromMedia( media, service_key ):
	existing_rating = None
	null_exists = False

	for m in media:
		ratings_manager = m.GetRatingsManager()

		rating = ratings_manager.GetRating( service_key )

		if rating is None:
			if existing_rating is not None:
				return ( MIXED, None )

			else:
				null_exists = True

		else:
			if null_exists:
				return ( MIXED, None )

			else:
				if existing_rating is None:
					existing_rating = rating

				else:
					if existing_rating != rating:
						return ( MIXED, None )

	if null_exists:
		return ( NULL, None )

	else:
		return ( SET, existing_rating )

def GetNumericalWidth( service_key ):
	service = HG.client_controller.services_manager.GetService( service_key )

	num_stars = service.GetNumStars()

	return 4 + 12 * num_stars

def GetPenAndBrushColours( service_key, rating_state ):
	service = HG.client_controller.services_manager.GetService( service_key )

	colour = service.GetColour( rating_state )

	( pen_rgb, brush_rgb ) = colour

	pen_colour = wx.Colour( *pen_rgb )
	brush_colour = wx.Colour( *brush_rgb )

	return ( pen_colour, brush_colour )

def GetShape( service_key ):
	service = HG.client_controller.services_manager.GetService( service_key )

	shape = service.GetShape()

	return shape

def GetStars( service_key, rating_state, rating ):
	service = HG.client_controller.services_manager.GetService( service_key )

	allow_zero = service.AllowZero()

	shape = service.GetShape()

	num_stars = service.GetNumStars()

	stars = []

	if rating_state in ( NULL, MIXED ):
		( pen_colour, brush_colour ) = GetPenAndBrushColours( service_key, rating_state )

		stars.append( ( num_stars, pen_colour, brush_colour ) )

	else:
		if allow_zero:
			num_stars_on = int( round( rating * num_stars ) )

		else:
			num_stars_on = int( round( rating * ( num_stars - 1 ) ) ) + 1

		num_stars_off = num_stars - num_stars_on

		( pen_colour, brush_colour ) = GetPenAndBrushColours( service_key, LIKE )

		stars.append( ( num_stars_on, pen_colour, brush_colour ) )

		( pen_colour, brush_colour ) = GetPenAndBrushColours( service_key, DISLIKE )

		stars.append( ( num_stars_off, pen_colour, brush_colour ) )

	return ( shape, stars )

### CLASSES ###

class RatingsManager( object ):
	def __init__( self, service_keys_to_ratings ):
		self._service_keys_to_ratings = service_keys_to_ratings

	def Duplicate( self ):
		return RatingsManager( dict( self._service_keys_to_ratings ) )

	def GetRating( self, service_key ):
		if service_key in self._service_keys_to_ratings:
			return self._service_keys_to_ratings[ service_key ]

		else:
			return None

	def GetRatingSlice( self, service_keys ):
		return frozenset( { self._service_keys_to_ratings[ service_key ] for service_key in service_keys if service_key in self._service_keys_to_ratings } )

	def GetServiceKeysToRatings( self ):
		return self._service_keys_to_ratings

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

		if action == HC.CONTENT_UPDATE_ADD:
			( rating, hashes ) = row

			if rating is None and service_key in self._service_keys_to_ratings:
				del self._service_keys_to_ratings[ service_key ]
			else:
				self._service_keys_to_ratings[ service_key ] = rating

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


code:
from .Ratings import *

__all__ = [
	"Archive"
]

from . import *


code:
import collections, cv2, numpy, os, shutil, struct, wx

from lib.Hydrus.Data import ShowException, ToHumanBytes, ToHumanInt
from lib.Hydrus.Paths import CleanUpTempPath
from lib.Hydrus.Paths.Local import GetTempPath
from lib.Hydrus.Serialisable import SerialisableBase, SerialisableList

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

if cv2.__version__.startswith( '2' ):
	IMREAD_UNCHANGED = cv2.CV_LOAD_IMAGE_UNCHANGED

else:
	IMREAD_UNCHANGED = cv2.IMREAD_UNCHANGED

png_font = cv2.FONT_HERSHEY_TRIPLEX
greyscale_text_color = 0

title_size = 0.7
payload_description_size = 0.5
text_size = 0.4

### FUNCTIONS ###

def CreateTopImage( width, title, payload_description, text ):
	text_extent_bmp = HG.client_controller.bitmap_manager.GetBitmap( 20, 20, 24 )

	dc = wx.MemoryDC( text_extent_bmp )

	text_font = wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT )

	basic_font_size = text_font.GetPointSize()

	payload_description_font = wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT )

	payload_description_font.SetPointSize( int( basic_font_size * 1.4 ) )

	title_font = wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT )

	title_font.SetPointSize( int( basic_font_size * 2.0 ) )

	texts_to_draw = []

	current_y = 6

	for ( t, f ) in ( ( title, title_font ), ( payload_description, payload_description_font ), ( text, text_font ) ):
		dc.SetFont( f )

		wrapped_texts = WrapText( dc, t, width - 20 )
		( gumpf, line_height ) = dc.GetTextExtent( 'abcdefghijklmnopqrstuvwxyz' )

		wrapped_texts_with_ys = []

		if len( wrapped_texts ) > 0:
			current_y += 10

			for wrapped_text in wrapped_texts:
				wrapped_texts_with_ys.append( ( wrapped_text, current_y ) )

				current_y += line_height + 4

		texts_to_draw.append( ( wrapped_texts_with_ys, f ) )

	current_y += 6

	top_height = current_y

	del dc
	del text_extent_bmp

	#

	top_bmp = HG.client_controller.bitmap_manager.GetBitmap( width, top_height, 24 )

	dc = wx.MemoryDC( top_bmp )

	dc.SetBackground( wx.Brush( wx.WHITE ) )

	dc.Clear()

	#

	dc.DrawBitmap( CC.GlobalBMPs.file_repository, width - 16 - 5, 5 )

	#

	for ( wrapped_texts_with_ys, f ) in texts_to_draw:
		dc.SetFont( f )

		for ( wrapped_text, y ) in wrapped_texts_with_ys:
			( t_width, t_height ) = dc.GetTextExtent( wrapped_text )

			dc.DrawText( wrapped_text, ( width - t_width ) // 2, y )

	del dc

	data_bytearray = top_bmp.ConvertToImage().GetData()

	data_bytes = bytes( data_bytearray )

	top_image_rgb = numpy.fromstring( data_bytes, dtype = 'uint8' ).reshape( ( top_height, width, 3 ) )

	HG.client_controller.bitmap_manager.ReleaseBitmap( top_bmp )

	top_image = cv2.cvtColor( top_image_rgb, cv2.COLOR_RGB2GRAY )

	top_height_header = struct.pack( '!H', top_height )

	byte0 = top_height_header[0:1]
	byte1 = top_height_header[1:2]

	top_image[0][0] = ord( byte0 )
	top_image[0][1] = ord( byte1 )

	return top_image

def DumpToPng( width, payload_bytes, title, payload_description, text, path ):
	payload_bytes_length = len( payload_bytes )

	header_and_payload_bytes_length = payload_bytes_length + 4

	payload_height = int( header_and_payload_bytes_length / width )

	if ( header_and_payload_bytes_length / width ) % 1.0 > 0:
		payload_height += 1

	top_image = CreateTopImage( width, title, payload_description, text )

	payload_length_header = struct.pack( '!I', payload_bytes_length )

	num_empty_bytes = payload_height * width - header_and_payload_bytes_length

	header_and_payload_bytes = payload_length_header + payload_bytes + b'\x00' * num_empty_bytes

	payload_image = numpy.fromstring( header_and_payload_bytes, dtype = 'uint8' ).reshape( ( payload_height, width ) )

	finished_image = numpy.concatenate( ( top_image, payload_image ) )

	# this is to deal with unicode paths, which cv2 can't handle
	( os_file_handle, temp_path ) = GetTempPath( suffix = '.png' )

	try:
		cv2.imwrite( temp_path, finished_image, [ cv2.IMWRITE_PNG_COMPRESSION, 9 ] )

		shutil.copy2( temp_path, path )

	except Exception as e:
		ShowException( e )

		raise Exception( 'Could not save the png!' )

	finally:
		CleanUpTempPath( os_file_handle, temp_path )

def GetPayloadBytes( payload_obj ):
	if isinstance( payload_obj, bytes ):
		return payload_obj

	elif isinstance( payload_obj, str ):
		return bytes( payload_obj, 'utf-8' )

	else:
		return payload_obj.DumpToNetworkBytes()

def GetPayloadTypeString( payload_obj ):
	if isinstance( payload_obj, bytes ):
		return 'Bytes'

	elif isinstance( payload_obj, str ):
		return 'String'

	elif isinstance( payload_obj, SerialisableList ):
		type_string_counts = collections.Counter()

		for o in payload_obj:
			type_string_counts[ GetPayloadTypeString( o ) ] += 1

		type_string = ', '.join( ( ToHumanInt( count ) + ' ' + s for ( s, count ) in list( type_string_counts.items() ) ) )

		return 'A list of ' + type_string

	elif isinstance( payload_obj, SerialisableBase ):
		return payload_obj.SERIALISABLE_NAME

	else:
		return repr( type( payload_obj ) )

def GetPayloadDescriptionAndBytes( payload_obj ):
	payload_bytes = GetPayloadBytes( payload_obj )

	payload_description = GetPayloadTypeString( payload_obj ) + ' - ' + ToHumanBytes( len( payload_bytes ) )

	return ( payload_description, payload_bytes )

def LoadFromPng( path ):
	# this is to deal with unicode paths, which cv2 can't handle
	( os_file_handle, temp_path ) = GetTempPath()

	try:
		shutil.copy2( path, temp_path )

		numpy_image = cv2.imread( temp_path, flags = IMREAD_UNCHANGED )

	except Exception as e:
		ShowException( e )

		raise Exception( 'That did not appear to be a valid image!' )

	finally:
		CleanUpTempPath( os_file_handle, temp_path )

	try:
		try:
			( height, width ) = numpy_image.shape

		except Exception:
			raise Exception( 'The file did not appear to be monochrome!' )

		try:
			complete_data = numpy_image.tostring()

			top_height_header = complete_data[:2]

			( top_height, ) = struct.unpack( '!H', top_height_header )

			payload_and_header_bytes = complete_data[ width * top_height : ]

		except Exception:
			raise Exception( 'Header bytes were invalid!' )

		try:
			payload_length_header = payload_and_header_bytes[:4]

			( payload_bytes_length, ) = struct.unpack( '!I', payload_length_header )

			payload_bytes = payload_and_header_bytes[ 4 : 4 + payload_bytes_length ]

		except Exception:
			raise Exception( 'Payload bytes were invalid!' )

	except Exception as e:
		message = 'The image loaded, but it did not seem to be a hydrus serialised png! The error was: {}'.format( str( e ) )
		message += os.linesep * 2
		message += 'If you believe this is a legit non-resized, non-converted hydrus serialised png, please send it to hydrus_dev.'

		raise Exception( message )

	return payload_bytes

def TextExceedsWidth( dc, text, width ):
	( t_width, t_height ) = dc.GetTextExtent( text )

	return t_width > width

def WrapText( dc, text, width ):
	words = text.split( ' ' )

	lines = []

	next_line = []

	for word in words:
		if word == '':
			continue

		potential_next_line = list( next_line )

		potential_next_line.append( word )

		if TextExceedsWidth( dc, ' '.join( potential_next_line ), width ):
			if len( potential_next_line ) == 1: # one very long word

				lines.append( ' '.join( potential_next_line ) )

				next_line = []

			else:
				lines.append( ' '.join( next_line ) )

				next_line = [ word ]

		else:
			next_line = potential_next_line

	if len( next_line ) > 0:
		lines.append( ' '.join( next_line ) )

	return lines


code:
import json
import zlib

LZ4_OK = False

try:
	import lz4
	import lz4.block

	LZ4_OK = True

except Exception: # ImportError wasn't enough here as Linux went up the shoot with a __version__ doesn't exist bs
	print( 'Could not import lz4--nbd.' )

### CONSTANTS ###

import lib.Hydrus.Globals as HG

### FUNCTIONS ###

def CreateFromNetworkBytes( network_string ):
	try:
		obj_bytes = zlib.decompress( network_string )

	except zlib.error:
		if LZ4_OK:
			obj_bytes = lz4.block.decompress( network_string )

		else:
			raise

	obj_string = str( obj_bytes, 'utf-8' )

	return CreateFromString( obj_string )

def CreateFromString( obj_string ):
	obj_tuple = json.loads( obj_string )

	return CreateFromSerialisableTuple( obj_tuple )

def CreateFromSerialisableTuple( obj_tuple ):
	if len( obj_tuple ) == 3:
		( serialisable_type, version, serialisable_info ) = obj_tuple

		obj = HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ serialisable_type ]()

	else:
		( serialisable_type, name, version, serialisable_info ) = obj_tuple

		obj = HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ serialisable_type ]( name )

	obj.InitialiseFromSerialisableInfo( version, serialisable_info )

	return obj

def GetNonDupeName( original_name, disallowed_names ):
	i = 1

	non_dupe_name = original_name

	while non_dupe_name in disallowed_names:
		non_dupe_name = original_name + ' (' + str( i ) + ')'

		i += 1

	return non_dupe_name

def SetNonDupeName( obj, disallowed_names ):
	non_dupe_name = GetNonDupeName( obj.GetName(), disallowed_names )

	obj.SetName( non_dupe_name )

class SerialisableBase( object ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["BASE"]
	SERIALISABLE_NAME = 'Base Serialisable Object'
	SERIALISABLE_VERSION = 1

	def _GetSerialisableInfo( self ):
		raise NotImplementedError()

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		raise NotImplementedError()

	def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
		return old_serialisable_info

	def DumpToNetworkBytes( self ):
		obj_string = self.DumpToString()

		obj_bytes = bytes( obj_string, 'utf-8' )

		return zlib.compress( obj_bytes, 9 )

	def DumpToString( self ):
		obj_tuple = self.GetSerialisableTuple()

		return json.dumps( obj_tuple )

	def Duplicate( self ):
		return CreateFromString( self.DumpToString() )

	def GetSerialisableTuple( self ):
		if hasattr( self, '_lock' ):
			with getattr( self, '_lock' ):
				serialisable_info = self._GetSerialisableInfo()

		else:
			serialisable_info = self._GetSerialisableInfo()

		return ( self.SERIALISABLE_TYPE, self.SERIALISABLE_VERSION, serialisable_info )

	def InitialiseFromSerialisableInfo( self, version, serialisable_info ):
		while version < self.SERIALISABLE_VERSION:
			( version, serialisable_info ) = self._UpdateSerialisableInfo( version, serialisable_info )

		self._InitialiseFromSerialisableInfo( serialisable_info )

class SerialisableNamed( SerialisableBase ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["BASE_NAMED"]
	SERIALISABLE_NAME = 'Named Base Serialisable Object'

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

		self._name = name

	def GetSerialisableTuple( self ):
		return ( self.SERIALISABLE_TYPE, self._name, self.SERIALISABLE_VERSION, self._GetSerialisableInfo() )

	def GetName( self ):
		return self._name

	def SetName( self, name ):
		self._name = name

	def SetNonDupeName( self, disallowed_names ):
		self._name = GetNonDupeName( self._name, disallowed_names )

class SerialisableDictionary( SerialisableBase, dict ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["DICTIONARY"]
	SERIALISABLE_NAME = 'Serialisable Dictionary'
	SERIALISABLE_VERSION = 1

	def __init__( self, *args, **kwargs ):
		dict.__init__( self, *args, **kwargs )
		SerialisableBase.__init__( self )

	def _GetSerialisableInfo( self ):
		simple_key_simple_value_pairs = []
		simple_key_serialisable_value_pairs = []
		serialisable_key_simple_value_pairs = []
		serialisable_key_serialisable_value_pairs = []

		for ( key, value ) in list( self.items() ):
			if isinstance( key, SerialisableBase ):
				serialisable_key = key.GetSerialisableTuple()

				if isinstance( value, SerialisableBase ):
					serialisable_value = value.GetSerialisableTuple()

					serialisable_key_serialisable_value_pairs.append( ( serialisable_key, serialisable_value ) )

				else:
					serialisable_value = value

					serialisable_key_simple_value_pairs.append( ( serialisable_key, serialisable_value ) )

			else:
				serialisable_key = key

				if isinstance( value, SerialisableBase ):
					serialisable_value = value.GetSerialisableTuple()

					simple_key_serialisable_value_pairs.append( ( serialisable_key, serialisable_value ) )

				else:
					serialisable_value = value

					simple_key_simple_value_pairs.append( ( serialisable_key, serialisable_value ) )

		return ( simple_key_simple_value_pairs, simple_key_serialisable_value_pairs, serialisable_key_simple_value_pairs, serialisable_key_serialisable_value_pairs )

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		( simple_key_simple_value_pairs, simple_key_serialisable_value_pairs, serialisable_key_simple_value_pairs, serialisable_key_serialisable_value_pairs ) = serialisable_info

		for ( key, value ) in simple_key_simple_value_pairs:
			self[ key ] = value

		for ( key, serialisable_value ) in simple_key_serialisable_value_pairs:
			value = CreateFromSerialisableTuple( serialisable_value )

			self[ key ] = value

		for ( serialisable_key, value ) in serialisable_key_simple_value_pairs:
			key = CreateFromSerialisableTuple( serialisable_key )

			self[ key ] = value

		for ( serialisable_key, serialisable_value ) in serialisable_key_serialisable_value_pairs:
			key = CreateFromSerialisableTuple( serialisable_key )

			value = CreateFromSerialisableTuple( serialisable_value )

			self[ key ] = value

class SerialisableBytesDictionary( SerialisableBase, dict ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["BYTES_DICT"]
	SERIALISABLE_NAME = 'Serialisable Dictionary With Bytestring Key/Value Support'
	SERIALISABLE_VERSION = 1

	def __init__( self, *args, **kwargs ):
		dict.__init__( self, *args, **kwargs )
		SerialisableBase.__init__( self )

	def _GetSerialisableInfo( self ):
		pairs = []

		for ( key, value ) in list( self.items() ):
			if isinstance( key, int ):
				encoded_key = key

			else:
				encoded_key = key.hex()

			if isinstance( value, ( list, tuple, set ) ):
				encoded_value = [ item.hex() for item in value ]

			elif value is None:
				encoded_value = value

			else:
				encoded_value = value.hex()

			pairs.append( ( encoded_key, encoded_value ) )

		return pairs

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		for ( encoded_key, encoded_value ) in serialisable_info:
			if isinstance( encoded_key, int ):
				key = encoded_key

			else:
				key = bytes.fromhex( encoded_key )

			if isinstance( encoded_value, ( list, tuple, set ) ):
				value = [ bytes.fromhex( encoded_item ) for encoded_item in encoded_value ]

			elif encoded_value is None:
				value = encoded_value

			else:
				value = bytes.fromhex( encoded_value )

			self[ key ] = value

class SerialisableList( SerialisableBase, list ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["LIST"]
	SERIALISABLE_NAME = 'Serialisable List'
	SERIALISABLE_VERSION = 1

	def __init__( self, *args, **kwargs ):
		list.__init__( self, *args, **kwargs )
		SerialisableBase.__init__( self )

	def _GetSerialisableInfo( self ):
		return [ obj.GetSerialisableTuple() for obj in self ]

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		for obj_tuple in serialisable_info:
			self.append( CreateFromSerialisableTuple( obj_tuple ) )

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType[ "LIST" ] ] = SerialisableList
HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType[ "BYTES_DICT" ] ] = SerialisableBytesDictionary
HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType[ "DICTIONARY" ] ] = SerialisableDictionary


code:
from .Serialisable import *

__all__ = [
	"Local"
]

from . import *


code:
from lib.Hydrus.Server import HydrusService
from lib.Hydrus.Server.Resources.Local import HydrusResourceBooruGallery, \
	HydrusResourceBooruPage, HydrusResourceBooruFile, HydrusResourceBooruThumbnail, \
	HydrusResourceClientAPIVersion, HydrusResourceClientAPIVerify, \
	HydrusResourceClientAPIPermissionsRequest, HydrusResourceClientAPIRestrictedAddFile, \
	HydrusResourceClientAPIRestrictedAddTagsAddTags, HydrusResourceClientAPIRestrictedAddTagsCleanTags, \
	HydrusResourceClientAPIRestrictedAddTagsGetTagServices, HydrusResourceClientAPIRestrictedAddURLsGetURLInfo, \
	HydrusResourceClientAPIRestrictedAddURLsGetURLFiles, HydrusResourceClientAPIRestrictedAddURLsImportURL, \
	HydrusResourceClientAPIRestrictedAddURLsAssociateURL, HydrusResourceClientAPIRestrictedGetFilesSearchFiles, \
	HydrusResourceClientAPIRestrictedGetFilesFileMetadata, HydrusResourceClientAPIRestrictedGetFilesGetFile, \
	HydrusResourceClientAPIRestrictedGetFilesGetThumbnail

from twisted.web.resource import NoResource

### CONSTANTS ###

from lib.Hydrus.Server import LOCAL_DOMAIN, REMOTE_DOMAIN
from lib.Hydrus.Server.Resources.Local import local_booru_css

### CLASSES ###

class HydrusClientService( HydrusService ):
	def __init__( self, service, allow_non_local_connections ):
		if allow_non_local_connections:
			self._client_requests_domain = REMOTE_DOMAIN

		else:
			self._client_requests_domain = LOCAL_DOMAIN

		HydrusService.__init__( self, service )

class HydrusServiceBooru( HydrusClientService ):
	def _InitRoot( self ):
		root = HydrusClientService._InitRoot( self )

		root.putChild( b'gallery', HydrusResourceBooruGallery( self._service, self._client_requests_domain ) )
		root.putChild( b'page', HydrusResourceBooruPage( self._service, self._client_requests_domain ) )
		root.putChild( b'file', HydrusResourceBooruFile( self._service, self._client_requests_domain ) )
		root.putChild( b'thumbnail', HydrusResourceBooruThumbnail( self._service, self._client_requests_domain ) )
		root.putChild( b'style.css', local_booru_css )

		return root

class HydrusServiceClientAPI( HydrusClientService ):
	def _InitRoot( self ):
		root = HydrusClientService._InitRoot( self )

		root.putChild( b'api_version', HydrusResourceClientAPIVersion( self._service, self._client_requests_domain ) )
		root.putChild( b'request_new_permissions', HydrusResourceClientAPIPermissionsRequest( self._service, self._client_requests_domain ) )
		root.putChild( b'verify_access_key', HydrusResourceClientAPIVerify( self._service, self._client_requests_domain ) )

		add_files = NoResource()

		root.putChild( b'add_files', add_files )

		add_files.putChild( b'add_file', HydrusResourceClientAPIRestrictedAddFile( self._service, self._client_requests_domain ) )

		add_tags = NoResource()

		root.putChild( b'add_tags', add_tags )

		add_tags.putChild( b'add_tags', HydrusResourceClientAPIRestrictedAddTagsAddTags( self._service, self._client_requests_domain ) )
		add_tags.putChild( b'clean_tags', HydrusResourceClientAPIRestrictedAddTagsCleanTags( self._service, self._client_requests_domain ) )
		add_tags.putChild( b'get_tag_services', HydrusResourceClientAPIRestrictedAddTagsGetTagServices( self._service, self._client_requests_domain ) )

		add_urls = NoResource()

		root.putChild( b'add_urls', add_urls )

		add_urls.putChild( b'get_url_info', HydrusResourceClientAPIRestrictedAddURLsGetURLInfo( self._service, self._client_requests_domain ) )
		add_urls.putChild( b'get_url_files', HydrusResourceClientAPIRestrictedAddURLsGetURLFiles( self._service, self._client_requests_domain ) )
		add_urls.putChild( b'add_url', HydrusResourceClientAPIRestrictedAddURLsImportURL( self._service, self._client_requests_domain ) )
		add_urls.putChild( b'associate_url', HydrusResourceClientAPIRestrictedAddURLsAssociateURL( self._service, self._client_requests_domain ) )

		get_files = NoResource()

		root.putChild( b'get_files', get_files )

		get_files.putChild( b'search_files', HydrusResourceClientAPIRestrictedGetFilesSearchFiles( self._service, self._client_requests_domain ) )
		get_files.putChild( b'file_metadata', HydrusResourceClientAPIRestrictedGetFilesFileMetadata( self._service, self._client_requests_domain ) )
		get_files.putChild( b'file', HydrusResourceClientAPIRestrictedGetFilesGetFile( self._service, self._client_requests_domain ) )
		get_files.putChild( b'thumbnail', HydrusResourceClientAPIRestrictedGetFilesGetThumbnail( self._service, self._client_requests_domain ) )

		return root


code:
from lib.Hydrus.Server import HydrusService
from lib.Hydrus.Server.Resources import HydrusResourceRestrictedUpdate
from lib.Hydrus.Server.RemoteResources import HydrusResourceAccessKey, \
	HydrusResourceAccessKeyVerification, HydrusResourceBusyCheck, \
	HydrusResourceRestrictedAccount, HydrusResourceRestrictedAccountInfo, \
	HydrusResourceRestrictedAccountTypes, HydrusResourceRestrictedBackup, \
	HydrusResourceRestrictedIP, HydrusResourceRestrictedMetadataUpdate, \
	HydrusResourceRestrictedNumPetitions, HydrusResourceRestrictedPetition, \
	HydrusResourceRestrictedRegistrationKeys, HydrusResourceRestrictedRepositoryFile, \
	HydrusResourceRestrictedRepositoryThumbnail, HydrusResourceRestrictedServices, \
	HydrusResourceSessionKey, HydrusResourceShutdown

### CONSTANTS ###

from lib.Hydrus.Server import LOCAL_DOMAIN, REMOTE_DOMAIN

### CLASSES ###

class HydrusServiceRestricted( HydrusService ):
	def _InitRoot( self ):
		root = HydrusService._InitRoot( self )

		root.putChild( b'access_key', HydrusResourceAccessKey( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'access_key_verification', HydrusResourceAccessKeyVerification( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'session_key', HydrusResourceSessionKey( self._service, REMOTE_DOMAIN ) )

		root.putChild( b'account', HydrusResourceRestrictedAccount( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'account_info', HydrusResourceRestrictedAccountInfo( self._service, REMOTE_DOMAIN ) )
		#root.putChild( b'account_modification', HydrusResourceRestrictedAccountModification( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'account_types', HydrusResourceRestrictedAccountTypes( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'registration_keys', HydrusResourceRestrictedRegistrationKeys( self._service, REMOTE_DOMAIN ) )

		return root

class HydrusServiceAdmin( HydrusServiceRestricted ):
	def _InitRoot( self ):
		root = HydrusServiceRestricted._InitRoot( self )

		root.putChild( b'busy', HydrusResourceBusyCheck() )
		root.putChild( b'backup', HydrusResourceRestrictedBackup( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'services', HydrusResourceRestrictedServices( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'shutdown', HydrusResourceShutdown( self._service, LOCAL_DOMAIN ) )

		return root

class HydrusServiceRepository( HydrusServiceRestricted ):
	def _InitRoot( self ):
		root = HydrusServiceRestricted._InitRoot( self )

		root.putChild( b'num_petitions', HydrusResourceRestrictedNumPetitions( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'petition', HydrusResourceRestrictedPetition( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'update', HydrusResourceRestrictedUpdate( self._service, REMOTE_DOMAIN ) )
		#root.putChild( b'immediate_update', HydrusResourceRestrictedImmediateUpdate( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'metadata', HydrusResourceRestrictedMetadataUpdate( self._service, REMOTE_DOMAIN ) )

		return root

class HydrusServiceRepositoryFile( HydrusServiceRepository ):
	def _InitRoot( self ):
		root = HydrusServiceRepository._InitRoot( self )

		root.putChild( b'file', HydrusResourceRestrictedRepositoryFile( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'ip', HydrusResourceRestrictedIP( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'thumbnail', HydrusResourceRestrictedRepositoryThumbnail( self._service, REMOTE_DOMAIN ) )

		return root

class HydrusServiceRepositoryTag( HydrusServiceRepository ):
	pass