code:
diff --git a/help/changelog.html b/help/changelog.html
index de8ed01..6a2f813 100755
--- a/help/changelog.html
+++ b/help/changelog.html
@@ -8,6 +8,30 @@

changelog

+

version 351

+

+
wrote a new (always on top!) hover window for the duplicate filter that sits on the middle-right. the duplicate cog button and action buttons are moved to this new window, as are the file comparison statements

+
the duplicate file comparison statements now state the relevant actual metadata along with better '>>'-style operators to highlight differences and green/red/blue colouring based on given score. it is now much easier to see and action clearly better files at-a-glance

+
improved some hover window focus display calculations to play with the new always-on-top tech

+
both the 'show some random dupes' button and finding dupe pairs for the filter should be a bit faster for very large search domains. the basic file search and indexing still has to run, but the second sampling step in both cases will bail out earlier once it has a decent result

+
core image handling functions now uniformly use OpenCV (faster, more accurate) by default, falling to PIL/Pillow on errors. image importing in the client and server should be a bit faster, and some unusual image rotations should now be read correctly

+
the server now supports OpenCV for image operations, it _should_ also still work with only PIL/Pillow, if you are running from source and cannot get it

+
unified all thumbnail generation code and insulated it from suprises due to unexpectedly-sized source files, fixing a potential client-level thumbnail generation looping bug

+
gave all image processing a refactor and general cleanup pass, deleted a bunch of old code

+
wrote a new 'local tag cache' for the db that will speed up tag definition lookups for all local files. this should speed up a variety of tag and file result fetching, particularly right after client boot. it will take a minute or two on update to generate

+
sped up how fast the tag parent structure builds itself

+
the review services panel now uses nested notebooks, rather than the old badly coded listbook control. I don't really like how it looks, but the code is now saner

+
similar-files metadata generation now discards blank frames more reliably

+
subscription popups now report x/y progress in terms of the current job, discarding historical work previously done. 1001/1003 is gone, 1/3 is in

+
made the disk cache more conservative on non-pre-processing calls

+
cleaned up some file import code, moving responsibility from the file locations manager to the file import object

+
updated the ipfs service listctrl to use the new listctrl object. also cleaned up its action code to be more async and stable

+
I believe I fixed a rare vector for the 'tryendmodal' dialog bug

+
fixed a bug in presenting the available importable downloader objects in the easy drag-and-drop downloader import when the multiple downloaders dropped included objects of the same type and name--duplicate-named objects in this case will now be discarded

+
unified url_match/url_class code differences to url class everywhere

+
updated some common db list selection code to use new python string formatting

+
plenty of misc code cleanup

+

version 350

the duplicate filter no longer applies the implicit system limit (which defaults to 10,000 files) on its search domains, which solves the undercounting issue on large search domains. duplicate operations will be appropriately slower, so narrow your duplicate file queries (adding a creator: tag works great) if they take too long

diff --git a/include/lib.Hydrus.Caches.py b/include/lib.Hydrus.Caches.py
index 8085a3f..2e2ac41 100644
--- a/include/lib.Hydrus.Caches.py
+++ b/include/lib.Hydrus.Caches.py
@@ -84,7 +84,10 @@ def BuildSimpleChildrenToParents( pairs ):
			 continue

-		if LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ):
			continue
+		if parent in simple_children_to_parents and LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ):
+
+			continue
+

		 simple_children_to_parents[ child ].add( parent )

@@ -185,13 +188,16 @@ def LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ):

	 potential_loop_paths = { parent }

-	while len( potential_loop_paths.intersection( list(simple_children_to_parents.keys()) ) ) > 0:
+	while True:

		 new_potential_loop_paths = set()

-		for potential_loop_path in potential_loop_paths.intersection( list(simple_children_to_parents.keys()) ):
+		for potential_loop_path in potential_loop_paths:

-			new_potential_loop_paths.update( simple_children_to_parents[ potential_loop_path ] )
+			if potential_loop_path in simple_children_to_parents:
+
+				new_potential_loop_paths.update( simple_children_to_parents[ potential_loop_path ] )
+

		 potential_loop_paths = new_potential_loop_paths
@@ -200,10 +206,12 @@ def LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ):

			 return True

+		elif len( potential_loop_paths ) == 0:
+
+			return False
+

-	return False
-
 class BitmapManager( object ):

	 MAX_MEMORY_ALLOWANCE = 512 * 1024 * 1024
@@ -604,11 +612,13 @@ class ClientFilesManager( object ):

		 bounding_dimensions = HG.client_controller.options[ 'thumbnail_dimensions' ]

+		target_resolution = lib.Handling.Image.GetThumbnailResolution( ( width, height ), bounding_dimensions )
+
		 percentage_in = self._controller.new_options.GetInteger( 'video_thumbnail_percentage_in' )

		 try:

-			thumbnail_bytes = lib.Handling.GenerateThumbnailBytes( file_path, bounding_dimensions, mime, width, height, duration, num_frames, percentage_in = percentage_in )
+			thumbnail_bytes = lib.Handling.GenerateThumbnailBytes( file_path, target_resolution, mime, duration, num_frames, percentage_in = percentage_in )

		 except Exception as e:

@@ -956,6 +966,19 @@ class ClientFilesManager( object ):

+	def AddFile( self, hash, mime, source_path, thumbnail_bytes = None ):
+
+		with self._rwlock.write:
+
+			self._AddFile( hash, mime, source_path )
+
+			if thumbnail_bytes is not None:
+
+				self._AddThumbnailFromBytes( hash, thumbnail_bytes )
+
+
+
+
	 def AddThumbnailFromBytes( self, hash, thumbnail_bytes ):

		 with self._rwlock.write:
@@ -1226,47 +1249,6 @@ class ClientFilesManager( object ):
		 return self._missing_locations

-	def ImportFile( self, file_import_job ):
-
-		if HG.file_report_mode:
-
-			lib.Hydrus.Data.ShowText( 'New file import job!' )
-
-
-		( pre_import_status, hash, note ) = file_import_job.GenerateHashAndStatus()
-
-		if file_import_job.IsNewToDB():
-
-			file_import_job.GenerateInfo()
-
-			file_import_job.CheckIsGoodToImport()
-
-			( temp_path, thumbnail ) = file_import_job.GetTempPathAndThumbnail()
-
-			mime = file_import_job.GetMime()
-
-			with self._rwlock.write:
-
-				self._AddFile( hash, mime, temp_path )
-
-				if thumbnail is not None:
-
-					self._AddThumbnailFromBytes( hash, thumbnail )
-
-
-
-			( import_status, note ) = self._controller.WriteSynchronous( 'import_file', file_import_job )
-
-		else:
-
-			import_status = pre_import_status
-
-
-		file_import_job.PubsubContentUpdates()
-
-		return ( import_status, hash, note )
-
-
	 def LocklessChangeFileExt( self, hash, old_mime, mime ):

		 old_path = self._GenerateExpectedFilePath( hash, old_mime )
@@ -1511,9 +1493,9 @@ class ClientFilesManager( object ):

			 path = self._GenerateExpectedThumbnailPath( hash )

-			numpy_image = lib.Handling.Image.Local.GenerateNumpyImage( path, mime )
+			numpy_image = lib.Handling.Image.Local.GenerateNumPyImage( path, mime )

-			( current_width, current_height ) = lib.Handling.Image.Local.GetNumPyImageResolution( numpy_image )
+			( current_width, current_height ) = lib.Handling.Image.GetResolutionNumPy( numpy_image )

			 bounding_dimensions = self._controller.options[ 'thumbnail_dimensions' ]

@@ -3262,7 +3244,7 @@ class ThumbnailCache( object ):

		 try:

-			numpy_image = lib.Handling.Image.Local.GenerateNumpyImage( path, mime )
+			numpy_image = lib.Handling.Image.Local.GenerateNumPyImage( path, mime )

		 except Exception as e:

@@ -3282,7 +3264,7 @@ class ThumbnailCache( object ):

			 try:

-				numpy_image = lib.Handling.Image.Local.GenerateNumpyImage( path, mime )
+				numpy_image = lib.Handling.Image.Local.GenerateNumPyImage( path, mime )

			 except Exception as e:

@@ -3294,7 +3276,7 @@ class ThumbnailCache( object ):

-		( current_width, current_height ) = lib.Handling.Image.Local.GetNumPyImageResolution( numpy_image )
+		( current_width, current_height ) = lib.Handling.Image.GetResolutionNumPy( numpy_image )

		 ( expected_width, expected_height ) = lib.Handling.Image.GetThumbnailResolution( ( media_width, media_height ), bounding_dimensions )

@@ -3319,7 +3301,7 @@ class ThumbnailCache( object ):

				 # this is _resize_, not _thumbnail_, because we already know the dimensions we want
				 # and in some edge cases, doing getthumbresolution on existing thumb dimensions results in float/int conversion imprecision and you get 90px/91px regen cycles that never get fixed
-				numpy_image = lib.Handling.Image.Local.ResizeNumpyImage( numpy_image, ( expected_width, expected_height ) )
+				numpy_image = lib.Handling.Image.ResizeNumPyImage( numpy_image, ( expected_width, expected_height ) )

				 if locations_manager.IsLocal():

@@ -3334,11 +3316,11 @@ class ThumbnailCache( object ):

						 try:

-							thumbnail_bytes = lib.Handling.Image.Local.GenerateBytesFromCV( numpy_image, mime )
+							thumbnail_bytes = lib.Handling.Image.GenerateThumbnailBytesNumPy( numpy_image, mime )

						 except lib.Hydrus.Exceptions.CantRenderWithCVException:

-							thumbnail_bytes = lib.Handling.GenerateThumbnailBytesFromStaticImagePathPIL( path, bounding_dimensions, mime )
+							thumbnail_bytes = lib.Handling.Image.GenerateThumbnailBytesFromStaticImagePath( path, ( expected_width, expected_height ), mime )

					 except Exception:
@@ -3383,7 +3365,7 @@ class ThumbnailCache( object ):

				 else:

-					numpy_image = lib.Handling.Image.Local.ResizeNumpyImage( numpy_image, ( expected_width, expected_height ) )
+					numpy_image = lib.Handling.Image.ResizeNumPyImage( numpy_image, ( expected_width, expected_height ) )

					 if locations_manager.IsLocal():

@@ -3555,9 +3537,13 @@ class ThumbnailCache( object ):

				 path = os.path.join( HC.STATIC_DIR, name + '.png' )

-				numpy_image = lib.Handling.Image.Local.GenerateNumpyImage( path, HC.IMAGE_PNG )
+				numpy_image = lib.Handling.Image.Local.GenerateNumPyImage( path, HC.IMAGE_PNG )
+
+				numpy_image_resolution = lib.Handling.Image.GetResolutionNumPy( numpy_image )
+
+				target_resolution = lib.Handling.Image.GetThumbnailResolution( numpy_image_resolution, bounding_dimensions )

-				numpy_image = lib.Handling.Image.Local.ThumbnailNumpyImage( numpy_image, bounding_dimensions )
+				numpy_image = lib.Handling.Image.ResizeNumPyImage( numpy_image, target_resolution )

				 hydrus_bitmap = lib.GUI.Rendering.GenerateHydrusBitmapFromNumPyImage( numpy_image )

diff --git a/include/lib.Hydrus.DB.Local.py b/include/lib.Hydrus.DB.Local.py
index 46e9cc3..8657f60 100755
--- a/include/lib.Hydrus.DB.Local.py
+++ b/include/lib.Hydrus.DB.Local.py
@@ -242,7 +242,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):
			 service_info_updates.append( ( num_files, service_id, HC.SERVICE_INFO_NUM_FILES ) )
			 service_info_updates.append( ( num_inbox, service_id, HC.SERVICE_INFO_NUM_INBOX ) )

-			select_statement = 'SELECT COUNT( * ) FROM files_info WHERE mime IN ' + lib.Hydrus.Data.SplayListForDB( HC.SEARCHABLE_MIMES ) + ' AND hash_id IN %s;'
+			select_statement = 'SELECT COUNT( * ) FROM files_info WHERE mime IN ' + lib.Hydrus.Data.SplayListForDB( HC.SEARCHABLE_MIMES ) + ' AND hash_id IN {};'

			 num_viewable_files = sum( self._STL( self._SelectFromList( select_statement, valid_hash_ids ) ) )

@@ -591,7 +591,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 if current_mappings_exist:

-					select_statement = 'SELECT tag_id, COUNT( * ) FROM ' + current_mappings_table_name + ' WHERE tag_id IN %s GROUP BY tag_id;'
+					select_statement = 'SELECT tag_id, COUNT( * ) FROM ' + current_mappings_table_name + ' WHERE tag_id IN {} GROUP BY tag_id;'

					 for ( tag_id, count ) in self._SelectFromList( select_statement, group_of_ids ):

@@ -608,7 +608,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 if pending_mappings_exist:

-					select_statement = 'SELECT tag_id, COUNT( * ) FROM ' + pending_mappings_table_name + ' WHERE tag_id IN %s GROUP BY tag_id;'
+					select_statement = 'SELECT tag_id, COUNT( * ) FROM ' + pending_mappings_table_name + ' WHERE tag_id IN {} GROUP BY tag_id;'

					 for ( tag_id, count ) in self._SelectFromList( select_statement, group_of_ids ):

@@ -636,7 +636,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ac_cache_table_name = GenerateCombinedFilesMappingsCacheTableName( service_id )

-		select_statement = 'SELECT tag_id, current_count, pending_count FROM ' + ac_cache_table_name + ' WHERE tag_id IN %s;'
+		select_statement = 'SELECT tag_id, current_count, pending_count FROM ' + ac_cache_table_name + ' WHERE tag_id IN {};'

		 return self._SelectFromListFetchAll( select_statement, tag_ids )

@@ -652,6 +652,50 @@ class DB( lib.Hydrus.DB.HydrusDB ):
		 self._c.executemany( 'DELETE FROM ' + ac_cache_table_name + ' WHERE tag_id = ? AND current_count = ? AND pending_count = ?;', ( ( tag_id, 0, 0 ) for ( tag_id, current_delta, pending_delta ) in count_ids ) )

+	def _CacheLocalTagIdsGenerate( self ):
+
+		self._c.execute( 'DELETE FROM local_tags_cache;' )
+
+		tag_ids = set()
+
+		tag_service_ids = self._GetServiceIds( HC.TAG_SERVICES )
+
+		for tag_service_id in tag_service_ids:
+
+			( cache_files_table_name, cache_current_mappings_table_name, cache_deleted_mappings_table_name, cache_pending_mappings_table_name, ac_cache_table_name ) = GenerateSpecificMappingsCacheTableNames( self._combined_local_file_service_id, tag_service_id )
+
+			service_tag_ids = self._STL( self._c.execute( 'SELECT tag_id FROM ' + ac_cache_table_name + ' WHERE current_count > 0;' ) )
+
+			tag_ids.update( service_tag_ids )
+
+
+		for block_of_tag_ids in lib.Hydrus.Data.SplitListIntoChunks( tag_ids, 1000 ):
+
+			self._CacheLocalTagIdsPotentialAdd( block_of_tag_ids )
+
+
+
+	def _CacheLocalTagIdsPotentialAdd( self, tag_ids ):
+
+		self._PopulateTagIdsToTagsCache( tag_ids )
+
+		self._c.executemany( 'INSERT OR IGNORE INTO local_tags_cache ( tag_id, tag ) VALUES ( ?, ? );', ( ( tag_id, self._tag_ids_to_tags_cache[ tag_id ] ) for tag_id in tag_ids ) )
+
+
+	def _CacheLocalTagIdsPotentialDelete( self, tag_ids ):
+
+		include_current = True
+		include_pending = False
+
+		ids_to_count = self._GetAutocompleteCounts( self._combined_tag_service_id, self._combined_local_file_service_id, tag_ids, include_current, include_pending )
+
+		useful_tag_ids = [ tag_id for ( tag_id, ( current_min, current_max, pending_min, pending_max ) ) in ids_to_count.items() if current_min > 0 ]
+
+		bad_tag_ids = set( tag_ids ).difference( useful_tag_ids )
+
+		self._c.executemany( 'DELETE FROM local_tags_cache WHERE tag_id = ?;', ( ( tag_id, ) for tag_id in bad_tag_ids ) )
+
+
	 def _CacheRepositoryAddHashes( self, service_id, service_hash_ids_to_hashes ):

		 ( hash_id_map_table_name, tag_id_map_table_name ) = GenerateRepositoryMasterCacheTableNames( service_id )
@@ -706,7 +750,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( hash_id_map_table_name, tag_id_map_table_name ) = GenerateRepositoryMasterCacheTableNames( service_id )

-		select_statement = 'SELECT hash_id FROM ' + hash_id_map_table_name + ' WHERE service_hash_id IN %s;'
+		select_statement = 'SELECT hash_id FROM ' + hash_id_map_table_name + ' WHERE service_hash_id IN {};'

		 hash_ids = self._STL( self._SelectFromList( select_statement, service_hash_ids ) )

@@ -1288,6 +1332,11 @@ class DB( lib.Hydrus.DB.HydrusDB ):
					 potential_hash_ids.add( larger_hash_id )

+				if len( potential_hash_ids ) == 1000:
+
+					break
+
+

		 if len( potential_hash_ids ) == 0:
@@ -1348,7 +1397,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

			 # distinct important here for the search results table join
-			result = self._c.execute( 'SELECT DISTINCT smaller_hash_id, larger_hash_id FROM ' + table_join + ' WHERE ' + predicate_string + ' AND duplicate_type = ? ORDER BY RANDOM() LIMIT 10000;', ( HC.DUPLICATE_UNKNOWN, ) ).fetchall()
+			result = self._c.execute( 'SELECT DISTINCT smaller_hash_id, larger_hash_id FROM ' + table_join + ' WHERE ' + predicate_string + ' AND duplicate_type = ? LIMIT 2500;', ( HC.DUPLICATE_UNKNOWN, ) ).fetchall()

		 # convert them into possible groups per each possible 'master hash_id', and value them
@@ -1874,7 +1923,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 self._c.executemany( 'DELETE FROM shape_maintenance_branch_regen WHERE phash_id = ?;', ( ( p_id, ) for p_id in unbalanced_phash_ids ) )

-		select_statement = 'SELECT phash_id FROM shape_perceptual_hash_map WHERE phash_id IN %s;'
+		select_statement = 'SELECT phash_id FROM shape_perceptual_hash_map WHERE phash_id IN {};'

		 useful_phash_ids = self._STS( self._SelectFromList( select_statement, unbalanced_phash_ids ) )

@@ -2015,7 +2064,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):
						 # adding a delay in seemed to fix it as well. guess it was some memory maintenance buffer/bytes thing
						 # anyway, we now just get the whole lot of results first and then work on the whole lot

-						select_statement = 'SELECT phash_id, phash, radius, inner_id, outer_id FROM shape_perceptual_hashes NATURAL JOIN shape_vptree WHERE phash_id IN %s;'
+						select_statement = 'SELECT phash_id, phash, radius, inner_id, outer_id FROM shape_perceptual_hashes NATURAL JOIN shape_vptree WHERE phash_id IN {};'

						 results = list( self._SelectFromList( select_statement, group_of_current_potentials ) )

@@ -2071,7 +2120,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):
				 lib.Hydrus.Data.ShowText( 'Similar file search completed in ' + lib.Hydrus.Data.ToHumanInt( num_cycles ) + ' cycles.' )

-			select_statement = 'SELECT hash_id FROM shape_perceptual_hash_map WHERE phash_id IN %s;'
+			select_statement = 'SELECT hash_id FROM shape_perceptual_hash_map WHERE phash_id IN {};'

			 similar_hash_ids = self._STL( self._SelectFromList( select_statement, similar_phash_ids ) )

@@ -2361,10 +2410,19 @@ class DB( lib.Hydrus.DB.HydrusDB ):

			 self._c.executemany( 'UPDATE ' + ac_cache_table_name + ' SET current_count = current_count + ?, pending_count = pending_count + ? WHERE tag_id = ?;', ( ( num_current, num_pending, tag_id ) for ( tag_id, num_current, num_pending ) in ac_cache_changes ) )

+			if file_service_id == self._combined_local_file_service_id:
+
+				potential_new_tag_ids = [ tag_id for ( tag_id, num_current, num_pending ) in ac_cache_changes if num_current > 0 ]
+
+				self._CacheLocalTagIdsPotentialAdd( potential_new_tag_ids )
+
+

	 def _CacheSpecificMappingsAddMappings( self, file_service_id, tag_service_id, mappings_ids ):

+		potential_new_tag_ids = []
+
		 ( cache_files_table_name, cache_current_mappings_table_name, cache_deleted_mappings_table_name, cache_pending_mappings_table_name, ac_cache_table_name ) = GenerateSpecificMappingsCacheTableNames( file_service_id, tag_service_id )

		 for ( tag_id, hash_ids ) in mappings_ids:
@@ -2385,12 +2443,19 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 if num_pending_rescinded > 0:

+					potential_new_tag_ids.append( tag_id )
+
					 self._c.execute( 'UPDATE ' + ac_cache_table_name + ' SET current_count = current_count + ?, pending_count = pending_count - ? WHERE tag_id = ?;', ( num_added, num_pending_rescinded, tag_id ) )

				 elif num_added > 0:

					 self._c.execute( 'INSERT OR IGNORE INTO ' + ac_cache_table_name + ' ( tag_id, current_count, pending_count ) VALUES ( ?, ?, ? );', ( tag_id, 0, 0 ) )

+					if self._GetRowCount() > 0:
+
+						potential_new_tag_ids.append( tag_id )
+
+
					 self._c.execute( 'UPDATE ' + ac_cache_table_name + ' SET current_count = current_count + ? WHERE tag_id = ?;', ( num_added, tag_id ) )

@@ -2400,6 +2465,11 @@ class DB( lib.Hydrus.DB.HydrusDB ):

+		if file_service_id == self._combined_local_file_service_id and len( potential_new_tag_ids ) > 0:
+
+			self._CacheLocalTagIdsPotentialAdd( potential_new_tag_ids )
+
+

	 def _CacheSpecificMappingsDrop( self, file_service_id, tag_service_id ):

@@ -2487,10 +2557,19 @@ class DB( lib.Hydrus.DB.HydrusDB ):

			 self._c.executemany( 'DELETE FROM ' + ac_cache_table_name + ' WHERE tag_id = ? AND current_count = ? AND pending_count = ?;', ( ( tag_id, 0, 0 ) for ( tag_id, num_current, num_pending ) in ac_cache_changes ) )

+			if file_service_id == self._combined_local_file_service_id:
+
+				potential_deleted_tag_ids = [ tag_id for ( tag_id, num_current, num_pending ) in ac_cache_changes if num_current > 0 ]
+
+				self._CacheLocalTagIdsPotentialDelete( potential_deleted_tag_ids )
+
+

	 def _CacheSpecificMappingsDeleteMappings( self, file_service_id, tag_service_id, mappings_ids ):

+		deleted_tag_ids = []
+
		 ( cache_files_table_name, cache_current_mappings_table_name, cache_deleted_mappings_table_name, cache_pending_mappings_table_name, ac_cache_table_name ) = GenerateSpecificMappingsCacheTableNames( file_service_id, tag_service_id )

		 for ( tag_id, hash_ids ) in mappings_ids:
@@ -2509,6 +2588,11 @@ class DB( lib.Hydrus.DB.HydrusDB ):

					 self._c.execute( 'DELETE FROM ' + ac_cache_table_name + ' WHERE tag_id = ? AND current_count = ? AND pending_count = ?;', ( tag_id, 0, 0 ) )

+					if self._GetRowCount() > 0:
+
+						deleted_tag_ids.append( tag_id )
+
+

				 #

@@ -2516,12 +2600,17 @@ class DB( lib.Hydrus.DB.HydrusDB ):

+		if file_service_id == self._combined_local_file_service_id and len( deleted_tag_ids ) > 0:
+
+			self._CacheLocalTagIdsPotentialDelete( deleted_tag_ids )
+
+

	 def _CacheSpecificMappingsFilterHashIds( self, file_service_id, tag_service_id, hash_ids ):

		 ( cache_files_table_name, cache_current_mappings_table_name, cache_deleted_mappings_table_name, cache_pending_mappings_table_name, ac_cache_table_name ) = GenerateSpecificMappingsCacheTableNames( file_service_id, tag_service_id )

-		select_statement = 'SELECT hash_id FROM ' + cache_files_table_name + ' WHERE hash_id IN %s;'
+		select_statement = 'SELECT hash_id FROM ' + cache_files_table_name + ' WHERE hash_id IN {};'

		 return self._STL( self._SelectFromList( select_statement, hash_ids ) )

@@ -2558,7 +2647,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( cache_files_table_name, cache_current_mappings_table_name, cache_deleted_mappings_table_name, cache_pending_mappings_table_name, ac_cache_table_name ) = GenerateSpecificMappingsCacheTableNames( file_service_id, tag_service_id )

-		select_statement = 'SELECT tag_id, current_count, pending_count FROM ' + ac_cache_table_name + ' WHERE tag_id IN %s;'
+		select_statement = 'SELECT tag_id, current_count, pending_count FROM ' + ac_cache_table_name + ' WHERE tag_id IN {};'

		 return self._SelectFromListFetchAll( select_statement, tag_ids )

@@ -3166,6 +3255,8 @@ class DB( lib.Hydrus.DB.HydrusDB ):
		 self._c.execute( 'CREATE TABLE IF NOT EXISTS external_caches.integer_subtags ( subtag_id INTEGER PRIMARY KEY, integer_subtag INTEGER );' )
		 self._CreateIndex( 'external_caches.integer_subtags', [ 'integer_subtag' ] )

+		self._c.execute( 'CREATE TABLE IF NOT EXISTS external_caches.local_tags_cache ( tag_id INTEGER PRIMARY KEY, tag TEXT UNIQUE );' )
+

	 def _DeleteFiles( self, service_id, hash_ids ):

@@ -3188,7 +3279,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 service_type = service.GetServiceType()

-		existing_hash_ids = self._STS( self._SelectFromList( 'SELECT hash_id FROM current_files WHERE service_id = ' + str( service_id ) + ' AND hash_id IN %s;', hash_ids ) )
+		existing_hash_ids = self._STS( self._SelectFromList( 'SELECT hash_id FROM current_files WHERE service_id = ' + str( service_id ) + ' AND hash_id IN {};', hash_ids ) )

		 service_info_updates = []

@@ -3212,7 +3303,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):
			 service_info_updates.append( ( -num_existing_files_removed, service_id, HC.SERVICE_INFO_NUM_FILES ) )
			 service_info_updates.append( ( -num_inbox, service_id, HC.SERVICE_INFO_NUM_INBOX ) )

-			select_statement = 'SELECT COUNT( * ) FROM files_info WHERE mime IN ' + lib.Hydrus.Data.SplayListForDB( HC.SEARCHABLE_MIMES ) + ' AND hash_id IN %s;'
+			select_statement = 'SELECT COUNT( * ) FROM files_info WHERE mime IN ' + lib.Hydrus.Data.SplayListForDB( HC.SEARCHABLE_MIMES ) + ' AND hash_id IN {};'

			 num_viewable_files = sum( self._STL( self._SelectFromList( select_statement, existing_hash_ids ) ) )

@@ -3674,7 +3765,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateMappingsTableNames( service_id )

-		child_hash_ids = self._STS( self._SelectFromList( 'SELECT hash_id FROM ' + current_mappings_table_name + ' WHERE tag_id IN %s;', sibling_tag_ids ) )
+		child_hash_ids = self._STS( self._SelectFromList( 'SELECT hash_id FROM ' + current_mappings_table_name + ' WHERE tag_id IN {};', sibling_tag_ids ) )

		 for parent_tag_id in parent_tag_ids:

@@ -4342,7 +4433,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 if hash_ids_to_current_file_service_ids is None:

-			hash_ids_to_current_file_service_ids = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( 'SELECT hash_id, service_id FROM current_files WHERE hash_id IN %s;', hash_ids ) )
+			hash_ids_to_current_file_service_ids = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( 'SELECT hash_id, service_id FROM current_files WHERE hash_id IN {};', hash_ids ) )

		 # Let's figure out if there is a common specific file service to this batch
@@ -4386,20 +4477,20 @@ class DB( lib.Hydrus.DB.HydrusDB ):

			 if common_file_service_id is None:

-				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_CURRENT, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + current_mappings_table_name + ' WHERE hash_id IN %s;', hash_ids ) )
-				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_DELETED, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + deleted_mappings_table_name + ' WHERE hash_id IN %s;', hash_ids ) )
-				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_PENDING, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + pending_mappings_table_name + ' WHERE hash_id IN %s;', hash_ids ) )
+				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_CURRENT, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + current_mappings_table_name + ' WHERE hash_id IN {};', hash_ids ) )
+				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_DELETED, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + deleted_mappings_table_name + ' WHERE hash_id IN {};', hash_ids ) )
+				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_PENDING, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + pending_mappings_table_name + ' WHERE hash_id IN {};', hash_ids ) )

			 else:

				 ( cache_files_table_name, cache_current_mappings_table_name, cache_deleted_mappings_table_name, cache_pending_mappings_table_name, ac_cache_table_name ) = GenerateSpecificMappingsCacheTableNames( common_file_service_id, tag_service_id )

-				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_CURRENT, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + cache_current_mappings_table_name + ' WHERE hash_id IN %s;', hash_ids ) )
-				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_DELETED, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + cache_deleted_mappings_table_name + ' WHERE hash_id IN %s;', hash_ids ) )
-				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_PENDING, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + cache_pending_mappings_table_name + ' WHERE hash_id IN %s;', hash_ids ) )
+				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_CURRENT, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + cache_current_mappings_table_name + ' WHERE hash_id IN {};', hash_ids ) )
+				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_DELETED, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + cache_deleted_mappings_table_name + ' WHERE hash_id IN {};', hash_ids ) )
+				tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_PENDING, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + cache_pending_mappings_table_name + ' WHERE hash_id IN {};', hash_ids ) )

-			tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_PETITIONED, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + petitioned_mappings_table_name + ' WHERE hash_id IN %s;', hash_ids ) )
+			tag_data.extend( ( hash_id, ( tag_service_id, HC.CONTENT_STATUS_PETITIONED, tag_id ) ) for ( hash_id, tag_id ) in self._SelectFromList( 'SELECT hash_id, tag_id FROM ' + petitioned_mappings_table_name + ' WHERE hash_id IN {};', hash_ids ) )

		 seen_tag_ids = { tag_id for ( hash_id, ( tag_service_id, status, tag_id ) ) in tag_data }
@@ -5149,7 +5240,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 if not done_files_info_predicates and ( need_file_domain_cross_reference or there_are_simple_files_info_preds_to_search_for ):

-			files_info_predicates.append( 'hash_id IN %s' )
+			files_info_predicates.append( 'hash_id IN {}' )

			 if file_service_key == CC.COMBINED_FILE_SERVICE_KEY:

@@ -5662,7 +5753,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 else:

-			selects = [ select.replace( ';', ' AND hash_id IN %s;' ) for select in selects ]
+			selects = [ select.replace( ';', ' AND hash_id IN {};' ) for select in selects ]

			 for select in selects:

@@ -5681,14 +5772,14 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 else:

-			query = self._SelectFromList( 'SELECT hash_id, url FROM url_map NATURAL JOIN urls WHERE hash_id in %s;', hash_ids )
+			query = self._SelectFromList( 'SELECT hash_id, url FROM url_map NATURAL JOIN urls WHERE hash_id in {};', hash_ids )

		 result_hash_ids = set()

		 for ( hash_id, url ) in query:

-			if rule_type == 'url_match':
+			if rule_type in ( 'url_class', 'url_match' ):

				 if rule.Matches( url ):

@@ -5808,7 +5899,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 table_union_to_select_from = '( ' + ' UNION ALL '.join( ( 'SELECT * FROM ' + table_name for table_name in table_names ) ) + ' )'

-		select_statement = 'SELECT hash_id, COUNT( DISTINCT tag_id ) FROM ' + table_union_to_select_from + ' WHERE hash_id IN %s GROUP BY hash_id;'
+		select_statement = 'SELECT hash_id, COUNT( DISTINCT tag_id ) FROM ' + table_union_to_select_from + ' WHERE hash_id IN {} GROUP BY hash_id;'

		 hash_id_tag_counts = list( self._SelectFromList( select_statement, hash_ids ) )

@@ -6315,23 +6406,23 @@ class DB( lib.Hydrus.DB.HydrusDB ):

			 self._PopulateHashIdsToHashesCache( hash_ids )

-			hash_ids_to_info = { hash_id : lib.Hydrus.Media.FileInfoManager( hash_id, self._hash_ids_to_hashes_cache[ hash_id ], size, mime, width, height, duration, num_frames, num_words ) for ( hash_id, size, mime, width, height, duration, num_frames, num_words ) in self._SelectFromList( 'SELECT * FROM files_info WHERE hash_id IN %s;', hash_ids ) }
+			hash_ids_to_info = { hash_id : lib.Hydrus.Media.FileInfoManager( hash_id, self._hash_ids_to_hashes_cache[ hash_id ], size, mime, width, height, duration, num_frames, num_words ) for ( hash_id, size, mime, width, height, duration, num_frames, num_words ) in self._SelectFromList( 'SELECT * FROM files_info WHERE hash_id IN {};', hash_ids ) }

-			hash_ids_to_current_file_service_ids_and_timestamps = lib.Hydrus.Data.BuildKeyToListDict( ( ( hash_id, ( service_id, timestamp ) ) for ( hash_id, service_id, timestamp ) in self._SelectFromList( 'SELECT hash_id, service_id, timestamp FROM current_files WHERE hash_id IN %s;', hash_ids ) ) )
+			hash_ids_to_current_file_service_ids_and_timestamps = lib.Hydrus.Data.BuildKeyToListDict( ( ( hash_id, ( service_id, timestamp ) ) for ( hash_id, service_id, timestamp ) in self._SelectFromList( 'SELECT hash_id, service_id, timestamp FROM current_files WHERE hash_id IN {};', hash_ids ) ) )

-			hash_ids_to_deleted_file_service_ids = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( 'SELECT hash_id, service_id FROM deleted_files WHERE hash_id IN %s;', hash_ids ) )
+			hash_ids_to_deleted_file_service_ids = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( 'SELECT hash_id, service_id FROM deleted_files WHERE hash_id IN {};', hash_ids ) )

-			hash_ids_to_pending_file_service_ids = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( 'SELECT hash_id, service_id FROM file_transfers WHERE hash_id IN %s;', hash_ids ) )
+			hash_ids_to_pending_file_service_ids = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( 'SELECT hash_id, service_id FROM file_transfers WHERE hash_id IN {};', hash_ids ) )

-			hash_ids_to_petitioned_file_service_ids = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( 'SELECT hash_id, service_id FROM file_petitions WHERE hash_id IN %s;', hash_ids ) )
+			hash_ids_to_petitioned_file_service_ids = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( 'SELECT hash_id, service_id FROM file_petitions WHERE hash_id IN {};', hash_ids ) )

-			hash_ids_to_urls = lib.Hydrus.Data.BuildKeyToSetDict( self._SelectFromList( 'SELECT hash_id, url FROM url_map NATURAL JOIN urls WHERE hash_id IN %s;', hash_ids ) )
+			hash_ids_to_urls = lib.Hydrus.Data.BuildKeyToSetDict( self._SelectFromList( 'SELECT hash_id, url FROM url_map NATURAL JOIN urls WHERE hash_id IN {};', hash_ids ) )

-			hash_ids_to_service_ids_and_filenames = lib.Hydrus.Data.BuildKeyToListDict( ( ( hash_id, ( service_id, filename ) ) for ( hash_id, service_id, filename ) in self._SelectFromList( 'SELECT hash_id, service_id, filename FROM service_filenames WHERE hash_id IN %s;', hash_ids ) ) )
+			hash_ids_to_service_ids_and_filenames = lib.Hydrus.Data.BuildKeyToListDict( ( ( hash_id, ( service_id, filename ) ) for ( hash_id, service_id, filename ) in self._SelectFromList( 'SELECT hash_id, service_id, filename FROM service_filenames WHERE hash_id IN {};', hash_ids ) ) )

-			hash_ids_to_local_ratings = lib.Hydrus.Data.BuildKeyToListDict( ( ( hash_id, ( service_id, rating ) ) for ( service_id, hash_id, rating ) in self._SelectFromList( 'SELECT service_id, hash_id, rating FROM local_ratings WHERE hash_id IN %s;', hash_ids ) ) )
+			hash_ids_to_local_ratings = lib.Hydrus.Data.BuildKeyToListDict( ( ( hash_id, ( service_id, rating ) ) for ( service_id, hash_id, rating ) in self._SelectFromList( 'SELECT service_id, hash_id, rating FROM local_ratings WHERE hash_id IN {};', hash_ids ) ) )

-			hash_ids_to_file_viewing_stats_managers = { hash_id : lib.Hydrus.Media.FileViewingStatsManager( preview_views, preview_viewtime, media_views, media_viewtime ) for ( hash_id, preview_views, preview_viewtime, media_views, media_viewtime ) in self._SelectFromList( 'SELECT hash_id, preview_views, preview_viewtime, media_views, media_viewtime FROM file_viewing_stats WHERE hash_id IN %s;', hash_ids ) }
+			hash_ids_to_file_viewing_stats_managers = { hash_id : lib.Hydrus.Media.FileViewingStatsManager( preview_views, preview_viewtime, media_views, media_viewtime ) for ( hash_id, preview_views, preview_viewtime, media_views, media_viewtime ) in self._SelectFromList( 'SELECT hash_id, preview_views, preview_viewtime, media_views, media_viewtime FROM file_viewing_stats WHERE hash_id IN {};', hash_ids ) }

			 #

@@ -7422,8 +7513,8 @@ class DB( lib.Hydrus.DB.HydrusDB ):
			 service_predicate = ' AND service_id = ' + str( service_id )

-		good_select = 'SELECT good_tag_id FROM tag_siblings WHERE bad_tag_id IN %s' + service_predicate + ';'
-		bad_select = 'SELECT bad_tag_id FROM tag_siblings WHERE good_tag_id IN %s' + service_predicate + ';'
+		good_select = 'SELECT good_tag_id FROM tag_siblings WHERE bad_tag_id IN {}' + service_predicate + ';'
+		bad_select = 'SELECT bad_tag_id FROM tag_siblings WHERE good_tag_id IN {}' + service_predicate + ';'

		 while len( search_tag_ids ) > 0:

@@ -7537,7 +7628,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 domain = lib.Networking.Domain.ConvertURLIntoDomain( url )

-			except lib.Hydrus.Exceptions.URLMatchException:
+			except lib.Hydrus.Exceptions.URLClassException:

				 domain = 'unknown.com'

@@ -7897,7 +7988,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

-	def _LoadIntoDiskCache( self, stop_time = None, caller_limit = None ):
+	def _LoadIntoDiskCache( self, stop_time = None, caller_limit = None, for_processing = False ):

		 self._CloseDBCursor()

@@ -7919,7 +8010,14 @@ class DB( lib.Hydrus.DB.HydrusDB ):
			 next_stop_time_presentation = 0
			 next_gc_collect = 0

-			paths = [ os.path.join( self._db_dir, filename ) for filename in list(self._db_filenames.values()) ]
+			filenames = self._db_filenames.values()
+
+			if not for_processing:
+
+				filenames = [ filename for filename in filenames if 'mappings' not in filename and 'master' not in filename ]
+
+
+			paths = [ os.path.join( self._db_dir, filename ) for filename in filenames ]

			 paths.sort( key = os.path.getsize )

@@ -8088,7 +8186,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

			 pubbed_error = False

-			select_statement = 'SELECT hash_id, hash FROM hashes WHERE hash_id IN %s;'
+			select_statement = 'SELECT hash_id, hash FROM hashes WHERE hash_id IN {};'

			 uncached_hash_ids_to_hashes = dict( self._SelectFromList( select_statement, uncached_hash_ids ) )

@@ -8134,7 +8232,18 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 if len( uncached_tag_ids ) > 0:

-			select_statement = 'SELECT tag_id, namespace, subtag FROM tags NATURAL JOIN namespaces NATURAL JOIN subtags WHERE tag_id IN %s;'
+			select_statement = 'SELECT tag_id, tag FROM local_tags_cache WHERE tag_id IN {};'
+
+			local_uncached_tag_ids_to_tags = { tag_id : tag for ( tag_id, tag ) in self._SelectFromList( select_statement, uncached_tag_ids ) }
+
+			self._tag_ids_to_tags_cache.update( local_uncached_tag_ids_to_tags )
+
+			uncached_tag_ids = [ tag_id for tag_id in tag_ids if tag_id not in self._tag_ids_to_tags_cache ]
+
+
+		if len( uncached_tag_ids ) > 0:
+
+			select_statement = 'SELECT tag_id, namespace, subtag FROM tags NATURAL JOIN namespaces NATURAL JOIN subtags WHERE tag_id IN {};'

			 uncached_tag_ids_to_tags = { tag_id : lib.Hydrus.Tags.CombineTag( namespace, subtag ) for ( tag_id, namespace, subtag ) in self._SelectFromList( select_statement, uncached_tag_ids ) }

@@ -9252,7 +9361,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

			 unprocessed_hash_ids = update_indices_to_unprocessed_hash_ids[ update_index ]

-			select_statement = 'SELECT hash_id FROM current_files WHERE service_id = ' + str( self._local_update_service_id ) + ' and hash_id IN %s;'
+			select_statement = 'SELECT hash_id FROM current_files WHERE service_id = ' + str( self._local_update_service_id ) + ' and hash_id IN {};'

			 local_hash_ids = self._STS( self._SelectFromList( select_statement, unprocessed_hash_ids ) )

@@ -9289,13 +9398,13 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 stop_time = lib.Hydrus.Data.GetNow() + min( 15 + ( num_updates_to_do * 30 ), 300 )

-				self._LoadIntoDiskCache( stop_time = stop_time )
+				self._LoadIntoDiskCache( stop_time = stop_time, for_processing = True )

				 num_updates_done = 0

				 client_files_manager = self._controller.client_files_manager

-				select_statement = 'SELECT hash_id FROM files_info WHERE mime = ' + str( HC.APPLICATION_HYDRUS_UPDATE_DEFINITIONS ) + ' AND hash_id IN %s;'
+				select_statement = 'SELECT hash_id FROM files_info WHERE mime = ' + str( HC.APPLICATION_HYDRUS_UPDATE_DEFINITIONS ) + ' AND hash_id IN {};'

				 definition_hash_ids = self._STL( self._SelectFromList( select_statement, hash_ids_i_can_process ) )

@@ -9382,7 +9491,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

-				select_statement = 'SELECT hash_id FROM files_info WHERE mime = ' + str( HC.APPLICATION_HYDRUS_UPDATE_CONTENT ) + ' AND hash_id IN %s;'
+				select_statement = 'SELECT hash_id FROM files_info WHERE mime = ' + str( HC.APPLICATION_HYDRUS_UPDATE_CONTENT ) + ' AND hash_id IN {};'

				 content_hash_ids = self._STL( self._SelectFromList( select_statement, hash_ids_i_can_process ) )

@@ -9605,6 +9714,10 @@ class DB( lib.Hydrus.DB.HydrusDB ):
				 self._CacheCombinedFilesMappingsGenerate( tag_service_id )

+			job_key.SetVariable( 'popup_text_1', 'generating local tag cache' )
+
+			self._CacheLocalTagIdsGenerate()
+
		 finally:

			 job_key.SetVariable( 'popup_text_1', 'done!' )
@@ -10529,15 +10642,15 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 domain_manager.Initialise()

-				url_matches = lib.Hydrus.Defaults.GetDefaultURLMatches()
+				url_classes = lib.Hydrus.Defaults.GetDefaultURLClasses()

-				url_matches = [ url_match for url_match in url_matches if 'xbooru' in url_match.GetName() ]
+				url_classes = [ url_class for url_class in url_classes if 'xbooru' in url_class.GetName() ]

-				existing_url_matches = [ url_match for url_match in domain_manager.GetURLMatches() if 'xbooru' not in url_match.GetName() ]
+				existing_url_classes = [ url_class for url_class in domain_manager.GetURLClasses() if 'xbooru' not in url_class.GetName() ]

-				url_matches.extend( existing_url_matches )
+				url_classes.extend( existing_url_classes )

-				domain_manager.SetURLMatches( url_matches )
+				domain_manager.SetURLClasses( url_classes )

				 self._SetJSONDump( domain_manager )

@@ -10615,15 +10728,15 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				url_matches = lib.Hydrus.Defaults.GetDefaultURLMatches()
+				url_classes = lib.Hydrus.Defaults.GetDefaultURLClasses()

-				url_matches = [ url_match for url_match in url_matches if 'pixiv file page' in url_match.GetName() or 'yiff.party' in url_match.GetName() ]
+				url_classes = [ url_class for url_class in url_classes if 'pixiv file page' in url_class.GetName() or 'yiff.party' in url_class.GetName() ]

-				existing_url_matches = [ url_match for url_match in domain_manager.GetURLMatches() if 'pixiv file page' not in url_match.GetName() and 'yiff.party' not in url_match.GetName() ]
+				existing_url_classes = [ url_class for url_class in domain_manager.GetURLClasses() if 'pixiv file page' not in url_class.GetName() and 'yiff.party' not in url_class.GetName() ]

-				url_matches.extend( existing_url_matches )
+				url_classes.extend( existing_url_classes )

-				domain_manager.SetURLMatches( url_matches )
+				domain_manager.SetURLClasses( url_classes )

				 #

@@ -10643,7 +10756,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -10669,7 +10782,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'pixiv file page', 'twitter tweet' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'pixiv file page', 'twitter tweet' ) )

				 #

@@ -10677,7 +10790,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -10711,7 +10824,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -10737,11 +10850,11 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'tumblr file page', 'artstation file page' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'tumblr file page', 'artstation file page' ) )

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -10767,7 +10880,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'inkbunny file page' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'inkbunny file page' ) )

				 #

@@ -10775,7 +10888,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -10802,7 +10915,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

						 domain = lib.Networking.Domain.ConvertURLIntoDomain( url )

-					except lib.Hydrus.Exceptions.URLMatchException:
+					except lib.Hydrus.Exceptions.URLClassException:

						 domain = 'unknown.com'

@@ -10859,7 +10972,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'inkbunny file page', 'artstation file page', 'artstation file page json api', 'pixiv file page', 'pixiv manga page', 'pixiv manga_big page' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'inkbunny file page', 'artstation file page', 'artstation file page json api', 'pixiv file page', 'pixiv manga page', 'pixiv manga_big page' ) )

				 #

@@ -10867,7 +10980,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -10899,21 +11012,21 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'inkbunny file page' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'inkbunny file page' ) )

				 #

				 domain_manager.OverwriteDefaultParsers( ( 'inkbunny file page parser', 'pixiv single file page parser - new layout' ) )

-				url_match = domain_manager.GetURLMatch( 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=69300276' )
+				url_class = domain_manager.GetURLClass( 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=69300276' )

-				if url_match is not None:
+				if url_class is not None:

					 new_pixiv_parser = domain_manager.GetParser( 'pixiv single file page parser - new layout' )

					 if new_pixiv_parser is not None:

-						domain_manager.OverwriteParserLink( url_match, new_pixiv_parser )
+						domain_manager.OverwriteParserLink( url_class, new_pixiv_parser )

@@ -10945,7 +11058,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -10971,7 +11084,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'deviant art file page', 'deviant art file page (old format)' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'deviant art file page', 'deviant art file page (old format)' ) )

				 #

@@ -10979,7 +11092,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11005,7 +11118,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'sakugabooru gallery page', 'sakugabooru file page', 'tumblr api gallery page', 'tumblr file page api', 'tumblr file page' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'sakugabooru gallery page', 'sakugabooru file page', 'tumblr api gallery page', 'tumblr file page api', 'tumblr file page' ) )

				 #

@@ -11013,7 +11126,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11047,7 +11160,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11073,7 +11186,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'artstation artist gallery page api', 'artstation artist gallery page', 'deviant art artist gallery page', 'deviant art artist gallery page - search initialisation', 'e621 gallery page - search initialisation' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'artstation artist gallery page api', 'artstation artist gallery page', 'deviant art artist gallery page', 'deviant art artist gallery page - search initialisation', 'e621 gallery page - search initialisation' ) )

				 #

@@ -11081,7 +11194,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11111,7 +11224,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'imgur media page api', 'imgur media page', 'imgur single media page', 'imgur tagged media page', 'imgur subreddit single media page', 'derpibooru file page' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'imgur media page api', 'imgur media page', 'imgur single media page', 'imgur tagged media page', 'imgur subreddit single media page', 'derpibooru file page' ) )

				 #

@@ -11119,7 +11232,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11145,7 +11258,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( ( 'xbooru gallery page', 'artstation artist gallery page api', 'tumblr api gallery page', 'gelbooru gallery page - search initialisation', 'gelbooru gallery page' ) )
+				domain_manager.OverwriteDefaultURLClasses( ( 'xbooru gallery page', 'artstation artist gallery page api', 'tumblr api gallery page', 'gelbooru gallery page - search initialisation', 'gelbooru gallery page' ) )

				 #

@@ -11153,7 +11266,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11274,19 +11387,19 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 # lots of gallery updates this week, so let's just do them all

-				default_url_matches = lib.Hydrus.Defaults.GetDefaultURLMatches()
+				default_url_classes = lib.Hydrus.Defaults.GetDefaultURLClasses()

-				overwrite_names = [ url_match.GetName() for url_match in default_url_matches if url_match.GetURLType() == HC.URL_TYPE_GALLERY ]
+				overwrite_names = [ url_class.GetName() for url_class in default_url_classes if url_class.GetURLType() == HC.URL_TYPE_GALLERY ]

-				domain_manager.OverwriteDefaultURLMatches( overwrite_names )
+				domain_manager.OverwriteDefaultURLClasses( overwrite_names )

				 # now clear out some failed experiments

-				url_matches = domain_manager.GetURLMatches()
+				url_classes = domain_manager.GetURLClasses()

-				url_matches = [ url_match for url_match in url_matches if 'search initialisation' not in url_match.GetName() ]
+				url_classes = [ url_class for url_class in url_classes if 'search initialisation' not in url_class.GetName() ]

-				domain_manager.SetURLMatches( url_matches )
+				domain_manager.SetURLClasses( url_classes )

				 #

@@ -11294,7 +11407,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11329,7 +11442,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ 'derpibooru gallery page' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ 'derpibooru gallery page' ] )

				 #

@@ -11395,7 +11508,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11429,7 +11542,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ 'gfycat file page', 'gfycat file page api', 'gfycat file page - gif url', 'gfycat file page - detail url' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ 'gfycat file page', 'gfycat file page api', 'gfycat file page - gif url', 'gfycat file page - detail url' ] )

				 #

@@ -11437,7 +11550,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11489,21 +11602,21 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 domain_manager.SetGUGs( new_gugs )

-				url_matches = domain_manager.GetURLMatches()
+				url_classes = domain_manager.GetURLClasses()

-				new_url_matches = []
+				new_url_classes = []

-				for url_match in url_matches:
+				for url_class in url_classes:

-					if url_match.GetURLType() == HC.URL_TYPE_GALLERY and 'pixiv.net' in url_match.GetExampleURL() and 'zzz' not in url_match.GetName():
+					if url_class.GetURLType() == HC.URL_TYPE_GALLERY and 'pixiv.net' in url_class.GetExampleURL() and 'zzz' not in url_class.GetName():

-						url_match.SetName( 'zzz - old url class - ' + url_match.GetName() )
+						url_class.SetName( 'zzz - old url class - ' + url_class.GetName() )

-					new_url_matches.append( url_match )
+					new_url_classes.append( url_class )

-				domain_manager.SetURLMatches( new_url_matches )
+				domain_manager.SetURLClasses( new_url_classes )

				 #

@@ -11511,7 +11624,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ 'twitter tweets api', 'pixiv artist page', 'pixiv artist gallery page api' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ 'twitter tweets api', 'pixiv artist page', 'pixiv artist gallery page api' ] )

				 #

@@ -11519,7 +11632,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11553,7 +11666,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11583,7 +11696,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ 'pixiv file page', 'pixiv file page api', 'pixiv tag search gallery page' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ 'pixiv file page', 'pixiv file page api', 'pixiv tag search gallery page' ] )

				 #

@@ -11591,7 +11704,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11655,7 +11768,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11681,7 +11794,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ 'imgur single media page gifv format', 'pixiv manga page' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ 'imgur single media page gifv format', 'pixiv manga page' ] )

				 #

@@ -11689,7 +11802,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11735,7 +11848,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11781,11 +11894,11 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ '4channel thread' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ '4channel thread' ] )

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11855,7 +11968,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ 'gelbooru gallery favorites page', 'gelbooru gallery pool page' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ 'gelbooru gallery favorites page', 'gelbooru gallery pool page' ] )

				 #

@@ -11867,7 +11980,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11921,7 +12034,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ 'nijie artist page', 'nijie view', 'nijie view popup' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ 'nijie artist page', 'nijie view', 'nijie view popup' ] )

				 #

@@ -11929,7 +12042,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -11959,7 +12072,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -12156,11 +12269,11 @@ class DB( lib.Hydrus.DB.HydrusDB ):

				 #

-				domain_manager.OverwriteDefaultURLMatches( [ 'yiff.party file attachment long' ] )
+				domain_manager.OverwriteDefaultURLClasses( [ 'yiff.party file attachment long' ] )

				 #

-				domain_manager.TryToLinkURLMatchesAndParsers()
+				domain_manager.TryToLinkURLClassesAndParsers()

				 #

@@ -12198,6 +12311,54 @@ class DB( lib.Hydrus.DB.HydrusDB ):

+		if version == 350:
+
+			self._controller.pub( 'splash_set_status_subtext', 'generating new local tag cache' )
+
+			try:
+
+				self._c.execute( 'CREATE TABLE IF NOT EXISTS external_caches.local_tags_cache ( tag_id INTEGER PRIMARY KEY, tag TEXT UNIQUE );' )
+
+				combined_local_file_service_id = self._GetServiceId( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
+
+				tag_ids = set()
+
+				tag_service_ids = self._GetServiceIds( HC.TAG_SERVICES )
+
+				for tag_service_id in tag_service_ids:
+
+					( cache_files_table_name, cache_current_mappings_table_name, cache_deleted_mappings_table_name, cache_pending_mappings_table_name, ac_cache_table_name ) = GenerateSpecificMappingsCacheTableNames( combined_local_file_service_id, tag_service_id )
+
+					service_tag_ids = self._STL( self._c.execute( 'SELECT tag_id FROM ' + ac_cache_table_name + ' WHERE current_count > 0;' ) )
+
+					tag_ids.update( service_tag_ids )
+
+
+				num_to_do = len( tag_ids )
+
+				i = 0
+
+				self._tag_ids_to_tags_cache = {}
+
+				for block_of_tag_ids in lib.Hydrus.Data.SplitListIntoChunks( tag_ids, 1000 ):
+
+					self._controller.pub( 'splash_set_status_subtext', 'generating new local tag cache: {}'.format( lib.Hydrus.Data.ConvertValueRangeToPrettyString( i, num_to_do ) ) )
+
+					self._PopulateTagIdsToTagsCache( block_of_tag_ids )
+
+					self._c.executemany( 'INSERT OR IGNORE INTO local_tags_cache ( tag_id, tag ) VALUES ( ?, ? );', ( ( tag_id, self._tag_ids_to_tags_cache[ tag_id ] ) for tag_id in block_of_tag_ids ) )
+
+					i += 1000
+
+
+			except Exception:
+
+				wx.SafeShowMessage( 'When trying to update, I could not create a new local tag cache! Error information will follow. Please let hydrus dev know!' )
+
+				raise
+
+
+
		 self._controller.pub( 'splash_set_title_text', 'updated db to v' + str( version + 1 ) )

		 self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
@@ -12323,7 +12484,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

			 for ( tag_id, hash_ids ) in pending_mappings_ids:

-				select_statement = 'SELECT hash_id FROM ' + current_mappings_table_name + ' WHERE tag_id = ' + str( tag_id ) + ' AND hash_id IN %s;'
+				select_statement = 'SELECT hash_id FROM ' + current_mappings_table_name + ' WHERE tag_id = ' + str( tag_id ) + ' AND hash_id IN {};'

				 existing_current_hash_ids = self._STS( self._SelectFromList( select_statement, hash_ids ) )

diff --git a/include/lib.Hydrus.Daemons.py b/include/lib.Hydrus.Daemons.py
index 761545e..7a8fc3f 100644
--- a/include/lib.Hydrus.Daemons.py
+++ b/include/lib.Hydrus.Daemons.py
@@ -154,7 +154,7 @@ def DAEMONDownloadFiles( controller ):

								 file_import_job = lib.Actions.Import.FileSeeds.FileImportJob( temp_path, file_import_options )

-								client_files_manager.ImportFile( file_import_job )
+								file_import_job.DoWork()

								 successful_hashes.add( hash )

diff --git a/include/lib.Hydrus.Defaults.py b/include/lib.Hydrus.Defaults.py
index 5758b39..ddc1449 100644
--- a/include/lib.Hydrus.Defaults.py
+++ b/include/lib.Hydrus.Defaults.py
@@ -441,13 +441,13 @@ def GetDefaultSimpleDownloaderFormulae():

	 return GetDefaultObjectsFromPNGs( dir_path, ( lib.Handling.Parsing.SimpleDownloaderParsingFormula, ) )

-def GetDefaultURLMatches():
+def GetDefaultURLClasses():

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

	 import lib.Networking.Domain

-	return GetDefaultObjectsFromPNGs( dir_path, ( lib.Networking.Domain.URLMatch, ) )
+	return GetDefaultObjectsFromPNGs( dir_path, ( lib.Networking.Domain.URLClass, ) )

 def GetDefaultObjectsFromPNGs( dir_path, allowed_object_types ):

@@ -619,7 +619,7 @@ def SetDefaultDomainManagerData( domain_manager ):

	 #

-	domain_manager.SetURLMatches( GetDefaultURLMatches() )
+	domain_manager.SetURLClasses( GetDefaultURLClasses() )

	 #

@@ -627,7 +627,7 @@ def SetDefaultDomainManagerData( domain_manager ):

	 #

-	domain_manager.TryToLinkURLMatchesAndParsers()
+	domain_manager.TryToLinkURLClassesAndParsers()

 def SetDefaultLoginManagerScripts( login_manager ):

diff --git a/include/lib.GUI.py b/include/lib.GUI.py
index 5113980..bef9dd7 100755
--- a/include/lib.GUI.py
+++ b/include/lib.GUI.py
@@ -2014,14 +2014,14 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):
			 submenu = wx.Menu()

			 lib.GUI.Menus.AppendMenuItem( self, submenu, 'manage gallery url generators', 'Manage the client\'s GUGs, which convert search terms into URLs.', self._ManageGUGs )
-			lib.GUI.Menus.AppendMenuItem( self, submenu, 'manage url classes', 'Configure which URLs the client can recognise.', self._ManageURLMatches )
+			lib.GUI.Menus.AppendMenuItem( self, submenu, 'manage url classes', 'Configure which URLs the client can recognise.', self._ManageURLClasses )
			 lib.GUI.Menus.AppendMenuItem( self, submenu, 'manage parsers', 'Manage the client\'s parsers, which convert URL content into hydrus metadata.', self._ManageParsers )

			 lib.GUI.Menus.AppendMenuItem( self, submenu, 'manage login scripts', 'Manage the client\'s login scripts, which define how to log in to different sites.', self._ManageLoginScripts )

			 lib.GUI.Menus.AppendSeparator( submenu )

-			lib.GUI.Menus.AppendMenuItem( self, submenu, 'manage url class links', 'Configure how URLs present across the client.', self._ManageURLMatchLinks )
+			lib.GUI.Menus.AppendMenuItem( self, submenu, 'manage url class links', 'Configure how URLs present across the client.', self._ManageURLClassLinks )
			 lib.GUI.Menus.AppendMenuItem( self, submenu, 'export downloaders', 'Export downloader components to easy-import pngs.', self._ExportDownloader )

			 lib.GUI.Menus.AppendSeparator( submenu )
@@ -2485,7 +2485,7 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):
			 message += os.linesep * 2
			 message += 'Since this URL cannot be parsed, a downloader cannot be created for it! Please check your url class links under the \'networking\' menu.'

-			raise lib.Hydrus.Exceptions.URLMatchException( message )
+			raise lib.Hydrus.Exceptions.URLClassException( message )

		 if url_type in ( HC.URL_TYPE_UNKNOWN, HC.URL_TYPE_FILE, HC.URL_TYPE_POST, HC.URL_TYPE_GALLERY ):
@@ -2616,22 +2616,22 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):

			 domain_manager = self._controller.network_engine.domain_manager

-			( file_post_default_tag_import_options, watchable_default_tag_import_options, url_match_keys_to_tag_import_options ) = domain_manager.GetDefaultTagImportOptions()
+			( file_post_default_tag_import_options, watchable_default_tag_import_options, url_class_keys_to_tag_import_options ) = domain_manager.GetDefaultTagImportOptions()

-			url_matches = domain_manager.GetURLMatches()
+			url_classes = domain_manager.GetURLClasses()
			 parsers = domain_manager.GetParsers()

-			url_match_keys_to_parser_keys = domain_manager.GetURLMatchKeysToParserKeys()
+			url_class_keys_to_parser_keys = domain_manager.GetURLClassKeysToParserKeys()

-			panel = lib.GUI.ScrolledPanels.Edit.EditDefaultTagImportOptionsPanel( dlg, url_matches, parsers, url_match_keys_to_parser_keys, file_post_default_tag_import_options, watchable_default_tag_import_options, url_match_keys_to_tag_import_options )
+			panel = lib.GUI.ScrolledPanels.Edit.EditDefaultTagImportOptionsPanel( dlg, url_classes, parsers, url_class_keys_to_parser_keys, file_post_default_tag_import_options, watchable_default_tag_import_options, url_class_keys_to_tag_import_options )

			 dlg.SetPanel( panel )

			 if dlg.ShowModal() == wx.ID_OK:

-				( file_post_default_tag_import_options, watchable_default_tag_import_options, url_match_keys_to_tag_import_options ) = panel.GetValue()
+				( file_post_default_tag_import_options, watchable_default_tag_import_options, url_class_keys_to_tag_import_options ) = panel.GetValue()

-				domain_manager.SetDefaultTagImportOptions( file_post_default_tag_import_options, watchable_default_tag_import_options, url_match_keys_to_tag_import_options )
+				domain_manager.SetDefaultTagImportOptions( file_post_default_tag_import_options, watchable_default_tag_import_options, url_class_keys_to_tag_import_options )

@@ -2648,20 +2648,20 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):

			 gug_keys_to_display = domain_manager.GetGUGKeysToDisplay()

-			url_matches = domain_manager.GetURLMatches()
+			url_classes = domain_manager.GetURLClasses()

-			url_match_keys_to_display = domain_manager.GetURLMatchKeysToDisplay()
+			url_class_keys_to_display = domain_manager.GetURLClassKeysToDisplay()

-			panel = lib.GUI.ScrolledPanels.Edit.EditDownloaderDisplayPanel( dlg, self._controller.network_engine, gugs, gug_keys_to_display, url_matches, url_match_keys_to_display )
+			panel = lib.GUI.ScrolledPanels.Edit.EditDownloaderDisplayPanel( dlg, self._controller.network_engine, gugs, gug_keys_to_display, url_classes, url_class_keys_to_display )

			 dlg.SetPanel( panel )

			 if dlg.ShowModal() == wx.ID_OK:

-				( gug_keys_to_display, url_match_keys_to_display ) = panel.GetValue()
+				( gug_keys_to_display, url_class_keys_to_display ) = panel.GetValue()

				 domain_manager.SetGUGKeysToDisplay( gug_keys_to_display )
-				domain_manager.SetURLMatchKeysToDisplay( url_match_keys_to_display )
+				domain_manager.SetURLClassKeysToDisplay( url_class_keys_to_display )

@@ -3213,7 +3213,7 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):

-	def _ManageURLMatches( self ):
+	def _ManageURLClasses( self ):

		 title = 'manage url classes'

@@ -3221,22 +3221,22 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):

			 domain_manager = self._controller.network_engine.domain_manager

-			url_matches = domain_manager.GetURLMatches()
+			url_classes = domain_manager.GetURLClasses()

-			panel = lib.GUI.ScrolledPanels.Edit.EditURLMatchesPanel( dlg, url_matches )
+			panel = lib.GUI.ScrolledPanels.Edit.EditURLClassesPanel( dlg, url_classes )

			 dlg.SetPanel( panel )

			 if dlg.ShowModal() == wx.ID_OK:

-				url_matches = panel.GetValue()
+				url_classes = panel.GetValue()

-				domain_manager.SetURLMatches( url_matches )
+				domain_manager.SetURLClasses( url_classes )

-	def _ManageURLMatchLinks( self ):
+	def _ManageURLClassLinks( self ):

		 title = 'manage url class links'

@@ -3244,20 +3244,20 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):

			 domain_manager = self._controller.network_engine.domain_manager

-			url_matches = domain_manager.GetURLMatches()
+			url_classes = domain_manager.GetURLClasses()
			 parsers = domain_manager.GetParsers()

-			url_match_keys_to_parser_keys = domain_manager.GetURLMatchKeysToParserKeys()
+			url_class_keys_to_parser_keys = domain_manager.GetURLClassKeysToParserKeys()

-			panel = lib.GUI.ScrolledPanels.Edit.EditURLMatchLinksPanel( dlg, self._controller.network_engine, url_matches, parsers, url_match_keys_to_parser_keys )
+			panel = lib.GUI.ScrolledPanels.Edit.EditURLClassLinksPanel( dlg, self._controller.network_engine, url_classes, parsers, url_class_keys_to_parser_keys )

			 dlg.SetPanel( panel )

			 if dlg.ShowModal() == wx.ID_OK:

-				url_match_keys_to_parser_keys = panel.GetValue()
+				url_class_keys_to_parser_keys = panel.GetValue()

-				domain_manager.SetURLMatchKeysToParserKeys( url_match_keys_to_parser_keys )
+				domain_manager.SetURLClassKeysToParserKeys( url_class_keys_to_parser_keys )

@@ -3681,7 +3681,7 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):

			 for i in range( 16 ):

-				t += 0.5
+				t += 1.0

				 for j in range( i + 1 ):

@@ -3692,12 +3692,12 @@ class FrameGUI( lib.GUI.TopLevelWindows.FrameThatResizes ):

				 HG.client_controller.CallLaterWXSafe( self, t, uias.Char, wx.WXK_RETURN )

-				t += 0.5
+				t += 1.0

				 HG.client_controller.CallLaterWXSafe( self, t, uias.Char, wx.WXK_RETURN )

-			t += 0.5
+			t += 1.0

			 HG.client_controller.CallLaterWXSafe( self, t, uias.Char, wx.WXK_DOWN )

diff --git a/include/lib.GUI.Canvas.py b/include/lib.GUI.Canvas.py
index f992ab6..dc43ed0 100755
--- a/include/lib.GUI.Canvas.py
+++ b/include/lib.GUI.Canvas.py
@@ -3091,6 +3091,8 @@ class CanvasFilterDuplicates( CanvasWithHovers ):

		 CanvasWithHovers.__init__( self, parent )

+		self._hover_duplicates = lib.GUI.HoverFrames.FullscreenHoverFrameRightDuplicates( self, self, self._canvas_key )
+
		 self._file_search_context = file_search_context
		 self._both_files_match = both_files_match

@@ -3295,33 +3297,6 @@ class CanvasFilterDuplicates( CanvasWithHovers ):

-	def _DrawAdditionalTopMiddleInfo( self, dc, current_y ):
-
-		if self._current_media is not None:
-
-			shown_media = self._current_media
-			comparison_media = self._media_list.GetNext( shown_media )
-
-			if shown_media != comparison_media:
-
-				( statements, score ) = lib.Hydrus.Media.GetDuplicateComparisonStatements( shown_media, comparison_media )
-
-				( client_width, client_height ) = self.GetClientSize()
-
-				for statement in statements:
-
-					( width, height ) = dc.GetTextExtent( statement )
-
-					dc.DrawText( statement, ( client_width - width ) // 2, current_y )
-
-					current_y += height + 3
-
-
-
-
-		return current_y
-
-
	 def _DrawBackgroundDetails( self, dc ):

		 if self._currently_fetching_pairs:
@@ -3599,7 +3574,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
			 first_media = lib.Hydrus.Media.MediaSingleton( first_media_result )
			 second_media = lib.Hydrus.Media.MediaSingleton( second_media_result )

-			( statements, score ) = lib.Hydrus.Media.GetDuplicateComparisonStatements( first_media, second_media )
+			score = lib.Hydrus.Media.GetDuplicateComparisonScore( first_media, second_media )

			 if score > 0:

diff --git a/include/lib.GUI.Common.py b/include/lib.GUI.Common.py
index 0b7fc47..963bd66 100755
--- a/include/lib.GUI.Common.py
+++ b/include/lib.GUI.Common.py
@@ -750,9 +750,18 @@ class BetterChoice( wx.Choice ):

		 selection = self.GetSelection()

-		if selection != wx.NOT_FOUND:
			return self.GetClientData( selection )
-		elif self.GetCount() > 0:
			return self.GetClientData( 0 )
-		else:
			return None
+		if selection != wx.NOT_FOUND:
+
+			return self.GetClientData( selection )
+
+		elif self.GetCount() > 0:
+
+			return self.GetClientData( 0 )
+
+		else:
+
+			return None
+

	 def SelectClientData( self, client_data ):
@@ -864,6 +873,19 @@ class BetterNotebook( wx.Notebook ):
		 self._ShiftSelection( -1 )

+	def SelectPage( self, page ):
+
+		for i in range( self.GetPageCount() ):
+
+			if self.GetPage( i ) == page:
+
+				self.SetSelection( i )
+
+				return
+
+
+
+
	 def SelectRight( self ):

		 self._ShiftSelection( 1 )
diff --git a/include/lib.GUI.Dialogs.py b/include/lib.GUI.Dialogs.py
index 4c41e63..6318836 100755
--- a/include/lib.GUI.Dialogs.py
+++ b/include/lib.GUI.Dialogs.py
@@ -1339,7 +1339,7 @@ class DialogInputTags( Dialog ):

		 self._tag_box = lib.GUI.ACDropdown.AutoCompleteDropdownTagsWrite( self, self.EnterTags, expand_parents, CC.LOCAL_FILE_SERVICE_KEY, service_key, null_entry_callable = self.OK )

-		self._ok = wx.Button( self, id= wx.ID_OK, label = 'OK' )
+		self._ok = wx.Button( self, id = wx.ID_OK, label = 'OK' )
		 self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		 self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'Cancel' )
@@ -2002,6 +2002,8 @@ class DialogYesNo( Dialog ):
		 self._no.SetForegroundColour( ( 128, 0, 0 ) )
		 self._no.SetLabelText( no_label )

+		self.SetEscapeId( wx.ID_NO )
+
		 #

		 hbox = wx.BoxSizer( wx.HORIZONTAL )
@@ -2056,6 +2058,8 @@ class DialogYesYesNo( Dialog ):
		 self._no.SetForegroundColour( ( 128, 0, 0 ) )
		 self._no.SetLabelText( no_label )

+		self.SetEscapeId( wx.ID_NO )
+
		 #

		 hbox = wx.BoxSizer( wx.HORIZONTAL )
diff --git a/include/lib.GUI.HoverFrames.py b/include/lib.GUI.HoverFrames.py
index f4449e1..a7a3bbb 100644
--- a/include/lib.GUI.HoverFrames.py
+++ b/include/lib.GUI.HoverFrames.py
@@ -36,6 +36,8 @@ class FullscreenHoverFrame( wx.Frame ):
		 self._canvas_key = canvas_key
		 self._current_media = None

+		self._always_on_top = False
+
		 self._last_ideal_position = None

		 self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_FRAMEBK ) )
@@ -66,6 +68,11 @@ class FullscreenHoverFrame( wx.Frame ):
				 self.SetSize( my_ideal_size )

+			if HC.PLATFORM_OSX and self.GetPosition() != my_ideal_position and self._always_on_top:
+
+				self.Raise()
+
+
			 self.SetPosition( my_ideal_position )

@@ -82,7 +89,7 @@ class FullscreenHoverFrame( wx.Frame ):

		 new_options = HG.client_controller.new_options

-		if new_options.GetBoolean( 'always_show_hover_windows' ):
+		if self._always_on_top or new_options.GetBoolean( 'always_show_hover_windows' ):

			 self._SizeAndPosition()

@@ -103,7 +110,7 @@ class FullscreenHoverFrame( wx.Frame ):

-		if self._current_media is None or not self.GetParent().IsShown():
+		if self._current_media is None or not self._my_canvas.IsShown():

			 if self.IsShown():

@@ -164,13 +171,15 @@ class FullscreenHoverFrame( wx.Frame ):

			 mime = self._current_media.GetMime()

-			mouse_is_over_interactable_media = mime == HC.APPLICATION_FLASH and self.GetParent().MouseIsOverMedia()
+			mouse_is_over_interactable_media = mime == HC.APPLICATION_FLASH and self._my_canvas.MouseIsOverMedia()

-			mouse_is_near_animation_bar = self.GetParent().MouseIsNearAnimationBar()
+			mouse_is_near_animation_bar = self._my_canvas.MouseIsNearAnimationBar()

			 mouse_is_over_something_important = mouse_is_over_interactable_media or mouse_is_near_animation_bar

-			focus_is_good = lib.GUI.Common.TLPHasFocus( self ) or lib.GUI.Common.TLPHasFocus( self.GetParent() )
+			current_focus = lib.GUI.Common.GetFocusTLP()
+
+			focus_is_good = lib.GUI.Common.IsWXAncestor( current_focus, self._my_canvas.GetTopLevelParent(), through_tlws = True )

			 ready_to_show = in_position and not mouse_is_over_something_important and focus_is_good and not dialog_open and not menu_open
			 ready_to_hide = not menu_open and ( not in_position or dialog_open or not focus_is_good )
@@ -221,6 +230,263 @@ class FullscreenHoverFrame( wx.Frame ):

+class FullscreenHoverFrameRightDuplicates( FullscreenHoverFrame ):
+
+	def __init__( self, parent, my_canvas, canvas_key ):
+
+		FullscreenHoverFrame.__init__( self, parent, my_canvas, canvas_key )
+
+		self._always_on_top = True
+
+		self._current_index_string = ''
+
+		self._comparison_media = None
+
+		menu_items = []
+
+		menu_items.append( ( 'normal', 'edit duplicate action options for \'this is better\'', 'edit what content is merged when you filter files', lib.Hydrus.Data.Call( self._EditMergeOptions, HC.DUPLICATE_BETTER ) ) )
+		menu_items.append( ( 'normal', 'edit duplicate action options for \'same quality\'', 'edit what content is merged when you filter files', lib.Hydrus.Data.Call( self._EditMergeOptions, HC.DUPLICATE_SAME_QUALITY ) ) )
+		menu_items.append( ( 'normal', 'edit duplicate action options for \'alternates\'', 'edit what content is merged when you filter files', lib.Hydrus.Data.Call( self._EditMergeOptions, HC.DUPLICATE_ALTERNATE ) ) )
+		menu_items.append( ( 'normal', 'edit duplicate action options for \'not duplicates\'', 'edit what content is merged when you filter files', lib.Hydrus.Data.Call( self._EditMergeOptions, HC.DUPLICATE_NOT_DUPLICATE ) ) )
+		menu_items.append( ( 'separator', None, None, None ) )
+		menu_items.append( ( 'normal', 'edit background lighten/darken switch intensity', 'edit how much the background will brighten or darken as you switch between the pair', self._EditBackgroundSwitchIntensity ) )
+
+		self._back_a_pair = lib.GUI.Common.BetterBitmapButton( self, CC.GlobalBMPs.first, HG.client_controller.pub, 'canvas_application_command', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_back' ), self._canvas_key )
+		self._back_a_pair.SetToolTip( 'go back a pair' )
+
+		self._previous_button = lib.GUI.Common.BetterBitmapButton( self, CC.GlobalBMPs.previous, HG.client_controller.pub, 'canvas_application_command', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_previous' ), self._canvas_key )
+		self._previous_button.SetToolTip( 'previous' )
+
+		self._index_text = lib.GUI.Common.BetterStaticText( self, 'index' )
+
+		self._next_button = lib.GUI.Common.BetterBitmapButton( self, CC.GlobalBMPs.next_bmp, HG.client_controller.pub, 'canvas_application_command', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'view_next' ), self._canvas_key )
+		self._next_button.SetToolTip( 'next' )
+
+		self._skip_a_pair = lib.GUI.Common.BetterBitmapButton( self, CC.GlobalBMPs.last, HG.client_controller.pub, 'canvas_application_command', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ), self._canvas_key )
+		self._skip_a_pair.SetToolTip( 'show a different pair' )
+
+		self._cog_button = lib.GUI.Common.MenuBitmapButton( self, CC.GlobalBMPs.cog, menu_items )
+
+		dupe_commands = []
+
+		dupe_commands.append( ( 'this is better', 'Set that the current file you are looking at is better than the other in the pair.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_this_is_better' ) ) )
+		dupe_commands.append( ( 'same quality', 'Set that the two files are duplicates of very similar quality.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_exactly_the_same' ) ) )
+		dupe_commands.append( ( 'alternates', 'Set that the files are not duplicates, but that one is derived from the other or that they are both descendants of a common ancestor.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_alternates' ) ) )
+		dupe_commands.append( ( 'not duplicates', 'Set that the files are not duplicates or otherwise related--that this pair is a false-positive match.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_not_dupes' ) ) )
+		dupe_commands.append( ( 'custom action', 'Choose one of the other actions but customise the merge and delete options for this specific decision.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_custom_action' ) ) )
+
+		command_button_vbox = wx.BoxSizer( wx.VERTICAL )
+
+		for ( label, tooltip, command ) in dupe_commands:
+
+			command_button = lib.GUI.Common.BetterButton( self, label, HG.client_controller.pub, 'canvas_application_command', command, self._canvas_key )
+
+			command_button.SetToolTip( tooltip )
+
+			command_button_vbox.Add( command_button, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+
+		self._comparison_statements_vbox = wx.BoxSizer( wx.VERTICAL )
+
+		self._comparison_statement_names = [ 'filesize', 'resolution', 'mime', 'num_tags', 'time_imported' ]
+
+		self._comparison_statements_sts = {}
+
+		for name in self._comparison_statement_names:
+
+			panel = wx.Panel( self )
+
+			st = lib.GUI.Common.BetterStaticText( panel, 'init' )
+
+			self._comparison_statements_sts[ name ] = ( panel, st )
+
+			hbox = wx.BoxSizer( wx.HORIZONTAL )
+
+			hbox.Add( ( 20, 20 ), CC.FLAGS_EXPAND_BOTH_WAYS )
+			hbox.Add( st, CC.FLAGS_VCENTER )
+			hbox.Add( ( 20, 20 ), CC.FLAGS_EXPAND_BOTH_WAYS )
+
+			panel.SetSizer( hbox )
+
+			panel.Hide()
+
+			self._comparison_statements_vbox.Add( panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
+
+
+		#
+
+		navigation_button_hbox = wx.BoxSizer( wx.HORIZONTAL )
+
+		navigation_button_hbox.Add( self._back_a_pair, CC.FLAGS_VCENTER )
+		navigation_button_hbox.Add( self._previous_button, CC.FLAGS_VCENTER )
+		navigation_button_hbox.Add( ( 20, 20 ), CC.FLAGS_EXPAND_BOTH_WAYS )
+		navigation_button_hbox.Add( self._index_text, CC.FLAGS_VCENTER )
+		navigation_button_hbox.Add( ( 20, 20 ), CC.FLAGS_EXPAND_BOTH_WAYS )
+		navigation_button_hbox.Add( self._next_button, CC.FLAGS_VCENTER )
+		navigation_button_hbox.Add( self._skip_a_pair, CC.FLAGS_VCENTER )
+		navigation_button_hbox.Add( self._cog_button, CC.FLAGS_VCENTER )
+
+		vbox = wx.BoxSizer( wx.VERTICAL )
+
+		vbox.Add( navigation_button_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+		vbox.Add( command_button_vbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+		vbox.Add( self._comparison_statements_vbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
+		self.SetSizer( vbox )
+
+		HG.client_controller.sub( self, 'SetDuplicatePair', 'canvas_new_duplicate_pair' )
+		HG.client_controller.sub( self, 'SetIndexString', 'canvas_new_index_string' )
+
+		self.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )
+
+
+	def _EditBackgroundSwitchIntensity( self ):
+
+		new_options = HG.client_controller.new_options
+
+		value = new_options.GetNoneableInteger( 'duplicate_background_switch_intensity' )
+
+		with lib.GUI.TopLevelWindows.DialogEdit( self, 'edit lighten/darken intensity' ) as dlg:
+
+			panel = lib.GUI.ScrolledPanels.Edit.EditNoneableIntegerPanel( dlg, value, message = 'intensity: ', none_phrase = 'do not change', min = 1, max = 9 )
+
+			dlg.SetPanel( panel )
+
+			if dlg.ShowModal() == wx.ID_OK:
+
+				new_value = panel.GetValue()
+
+				new_options.SetNoneableInteger( 'duplicate_background_switch_intensity', new_value )
+
+
+
+
+	def _EditMergeOptions( self, duplicate_type ):
+
+		new_options = HG.client_controller.new_options
+
+		duplicate_action_options = new_options.GetDuplicateActionOptions( duplicate_type )
+
+		with lib.GUI.TopLevelWindows.DialogEdit( self, 'edit duplicate merge options' ) as dlg:
+
+			panel = lib.GUI.ScrolledPanels.Edit.EditDuplicateActionOptionsPanel( dlg, duplicate_type, duplicate_action_options )
+
+			dlg.SetPanel( panel )
+
+			if dlg.ShowModal() == wx.ID_OK:
+
+				duplicate_action_options = panel.GetValue()
+
+				new_options.SetDuplicateActionOptions( duplicate_type, duplicate_action_options )
+
+
+
+
+	def _GetIdealSizeAndPosition( self ):
+
+		parent = self.GetParent()
+
+		( parent_width, parent_height ) = parent.GetClientSize()
+
+		( my_width, my_height ) = self.GetSize()
+
+		my_ideal_width = int( parent_width * 0.2 )
+
+		should_resize = my_ideal_width != my_width
+
+		ideal_size = ( my_ideal_width, -1 )
+		ideal_position = lib.GUI.Common.ClientToScreen( parent, ( int( parent_width * 0.8 ), int( parent_height * 0.3 ) ) )
+
+		return ( should_resize, ideal_size, ideal_position )
+
+
+	def _ResetComparisonStatements( self ):
+
+		fit_needed = False
+
+		statements_and_scores = lib.Hydrus.Media.GetDuplicateComparisonStatements( self._current_media, self._comparison_media )
+
+		for name in self._comparison_statement_names:
+
+			( panel, st ) = self._comparison_statements_sts[ name ]
+
+			if name in statements_and_scores:
+
+				( statement, score ) = statements_and_scores[ name ]
+
+				if not panel.IsShown():
+
+					fit_needed = True
+
+					panel.Show()
+
+
+				st.SetLabelText( statement )
+
+				if score > 0:
+
+					colour = ( 0, 128, 0 )
+
+				elif score < 0:
+
+					colour = ( 128, 0, 0 )
+
+				else:
+
+					colour = ( 0, 0, 128 )
+
+
+				st.SetForegroundColour( colour )
+
+			else:
+
+				if panel.IsShown():
+
+					fit_needed = True
+
+					panel.Hide()
+
+
+
+
+		if fit_needed:
+
+			self.Fit()
+
+		else:
+
+			self.Layout()
+
+
+
+	def EventMouseWheel( self, event ):
+
+		event.ResumePropagation( 1 )
+		event.Skip()
+
+
+	def SetDuplicatePair( self, canvas_key, shown_media, comparison_media ):
+
+		if canvas_key == self._canvas_key:
+
+			self._current_media = shown_media
+			self._comparison_media = comparison_media
+
+			self._ResetComparisonStatements()
+
+
+
+	def SetIndexString( self, canvas_key, text ):
+
+		if canvas_key == self._canvas_key:
+
+			self._current_index_string = text
+
+			self._index_text.SetLabelText( self._current_index_string )
+
+
+
 class FullscreenHoverFrameTop( FullscreenHoverFrame ):

	 def __init__( self, parent, my_canvas, canvas_key ):
@@ -644,90 +910,6 @@ class FullscreenHoverFrameTopNavigable( FullscreenHoverFrameTop ):

 class FullscreenHoverFrameTopDuplicatesFilter( FullscreenHoverFrameTopNavigable ):

-	def __init__( self, parent, my_canvas, canvas_key ):
-
-		FullscreenHoverFrameTopNavigable.__init__( self, parent, my_canvas, canvas_key )
-
-		HG.client_controller.sub( self, 'SetDuplicatePair', 'canvas_new_duplicate_pair' )
-
-
-	def _PopulateCenterButtons( self ):
-
-		menu_items = []
-
-		menu_items.append( ( 'normal', 'edit duplicate action options for \'this is better\'', 'edit what content is merged when you filter files', lib.Hydrus.Data.Call( self._EditMergeOptions, HC.DUPLICATE_BETTER ) ) )
-		menu_items.append( ( 'normal', 'edit duplicate action options for \'same quality\'', 'edit what content is merged when you filter files', lib.Hydrus.Data.Call( self._EditMergeOptions, HC.DUPLICATE_SAME_QUALITY ) ) )
-		menu_items.append( ( 'normal', 'edit duplicate action options for \'alternates\'', 'edit what content is merged when you filter files', lib.Hydrus.Data.Call( self._EditMergeOptions, HC.DUPLICATE_ALTERNATE ) ) )
-		menu_items.append( ( 'normal', 'edit duplicate action options for \'not duplicates\'', 'edit what content is merged when you filter files', lib.Hydrus.Data.Call( self._EditMergeOptions, HC.DUPLICATE_NOT_DUPLICATE ) ) )
-		menu_items.append( ( 'separator', None, None, None ) )
-		menu_items.append( ( 'normal', 'edit background lighten/darken switch intensity', 'edit how much the background will brighten or darken as you switch between the pair', self._EditBackgroundSwitchIntensity ) )
-
-		cog_button = lib.GUI.Common.MenuBitmapButton( self, CC.GlobalBMPs.cog, menu_items )
-
-		self._top_hbox.Add( cog_button, CC.FLAGS_SIZER_VCENTER )
-
-		FullscreenHoverFrameTopNavigable._PopulateCenterButtons( self )
-
-		dupe_commands = []
-
-		dupe_commands.append( ( 'this is better', 'Set that the current file you are looking at is better than the other in the pair.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_this_is_better' ) ) )
-		dupe_commands.append( ( 'same quality', 'Set that the two files are duplicates of very similar quality.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_exactly_the_same' ) ) )
-		dupe_commands.append( ( 'alternates', 'Set that the files are not duplicates, but that one is derived from the other or that they are both descendants of a common ancestor.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_alternates' ) ) )
-		dupe_commands.append( ( 'not duplicates', 'Set that the files are not duplicates or otherwise related--that this pair is a false-positive match.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_not_dupes' ) ) )
-		dupe_commands.append( ( 'custom action', 'Choose one of the other actions but customise the merge and delete options for this specific decision.', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_custom_action' ) ) )
-
-		for ( label, tooltip, command ) in dupe_commands:
-
-			command_button = lib.GUI.Common.BetterButton( self, label, HG.client_controller.pub, 'canvas_application_command', command, self._canvas_key )
-
-			command_button.SetToolTip( tooltip )
-
-			self._button_hbox.Add( command_button, CC.FLAGS_VCENTER )
-
-
-
-	def _EditBackgroundSwitchIntensity( self ):
-
-		new_options = HG.client_controller.new_options
-
-		value = new_options.GetNoneableInteger( 'duplicate_background_switch_intensity' )
-
-		with lib.GUI.TopLevelWindows.DialogEdit( self, 'edit lighten/darken intensity' ) as dlg:
-
-			panel = lib.GUI.ScrolledPanels.Edit.EditNoneableIntegerPanel( dlg, value, message = 'intensity: ', none_phrase = 'do not change', min = 1, max = 9 )
-
-			dlg.SetPanel( panel )
-
-			if dlg.ShowModal() == wx.ID_OK:
-
-				new_value = panel.GetValue()
-
-				new_options.SetNoneableInteger( 'duplicate_background_switch_intensity', new_value )
-
-
-
-
-	def _EditMergeOptions( self, duplicate_type ):
-
-		new_options = HG.client_controller.new_options
-
-		duplicate_action_options = new_options.GetDuplicateActionOptions( duplicate_type )
-
-		with lib.GUI.TopLevelWindows.DialogEdit( self, 'edit duplicate merge options' ) as dlg:
-
-			panel = lib.GUI.ScrolledPanels.Edit.EditDuplicateActionOptionsPanel( dlg, duplicate_type, duplicate_action_options )
-
-			dlg.SetPanel( panel )
-
-			if dlg.ShowModal() == wx.ID_OK:
-
-				duplicate_action_options = panel.GetValue()
-
-				new_options.SetDuplicateActionOptions( duplicate_type, duplicate_action_options )
-
-
-
-
	 def _PopulateLeftButtons( self ):

		 self._first_button = lib.GUI.Common.BetterBitmapButton( self, CC.GlobalBMPs.first, HG.client_controller.pub, 'canvas_application_command', lib.Hydrus.Data.Local.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_back' ), self._canvas_key )
@@ -743,33 +925,6 @@ class FullscreenHoverFrameTopDuplicatesFilter( FullscreenHoverFrameTopNavigable
		 self._top_hbox.Add( self._last_button, CC.FLAGS_VCENTER )

-	def SetDisplayMedia( self, canvas_key, media ):
-
-		if canvas_key == self._canvas_key:
-
-			if media is None:
-
-				self._additional_info_text.SetLabelText( '' )
-
-
-			FullscreenHoverFrameTopNavigable.SetDisplayMedia( self, canvas_key, media )
-
-
-
-	def SetDuplicatePair( self, canvas_key, shown_media, comparison_media ):
-
-		if canvas_key == self._canvas_key:
-
-			( statements, score ) = lib.Hydrus.Media.GetDuplicateComparisonStatements( shown_media, comparison_media )
-
-			self._additional_info_text.SetLabelText( os.linesep.join( statements ) )
-
-			self._ResetText()
-
-			self._ResetButtons()
-
-
-
 class FullscreenHoverFrameTopNavigableList( FullscreenHoverFrameTopNavigable ):

	 def _PopulateLeftButtons( self ):
diff --git a/include/lib.GUI.Media.py b/include/lib.GUI.Media.py
index f3c995a..115efe9 100755
--- a/include/lib.GUI.Media.py
+++ b/include/lib.GUI.Media.py
@@ -61,7 +61,7 @@ def CopyMediaURLs( medias ):

	 HG.client_controller.pub( 'clipboard', 'text', urls_string )

-def CopyMediaURLMatchURLs( medias, url_match ):
+def CopyMediaURLClassURLs( medias, url_class ):

	 urls = set()

@@ -71,7 +71,7 @@ def CopyMediaURLMatchURLs( medias, url_match ):

		 for url in media_urls:

-			if url_match.Matches( url ):
+			if url_class.Matches( url ):

				 urls.add( url )

@@ -97,15 +97,15 @@ def DoOpenKnownURLFromShortcut( win, media ):

		 for url in urls:

-			url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( url )
+			url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( url )

-			if url_match is None:
+			if url_class is None:

				 unmatched_urls.append( url )

			 else:

-				label = url_match.GetName() + ': ' + url
+				label = url_class.GetName() + ': ' + url

				 matched_labels_and_urls.append( ( label, url ) )

@@ -226,7 +226,7 @@ def OpenMediaURLs( medias ):

	 OpenURLs( urls )

-def OpenMediaURLMatchURLs( medias, url_match ):
+def OpenMediaURLClassURLs( medias, url_class ):

	 urls = set()

@@ -236,7 +236,7 @@ def OpenMediaURLMatchURLs( medias, url_match ):

		 for url in media_urls:

-			if url_match.Matches( url ):
+			if url_class.Matches( url ):

				 urls.add( url )

@@ -300,15 +300,15 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):

		 for url in focus_urls:

-			url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( url )
+			url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( url )

-			if url_match is None:
+			if url_class is None:

				 focus_unmatched_urls.append( url )

			 else:

-				label = url_match.GetName() + ': ' + url
+				label = url_class.GetName() + ': ' + url

				 focus_matched_labels_and_urls.append( ( label, url ) )

@@ -324,8 +324,8 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):

	 # figure out which urls these selected files have

-	selected_media_url_matches = set()
-	multiple_or_unmatching_selection_url_matches = False
+	selected_media_url_classes = set()
+	multiple_or_unmatching_selection_url_classes = False

	 if selected_media is not None and len( selected_media ) > 1:

@@ -348,26 +348,26 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):

			 for url in media_urls:

-				url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( url )
+				url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( url )

-				if url_match is None:
+				if url_class is None:

-					multiple_or_unmatching_selection_url_matches = True
+					multiple_or_unmatching_selection_url_classes = True

				 else:

-					selected_media_url_matches.add( url_match )
+					selected_media_url_classes.add( url_class )

-		if len( selected_media_url_matches ) > 1:
+		if len( selected_media_url_classes ) > 1:

-			multiple_or_unmatching_selection_url_matches = True
+			multiple_or_unmatching_selection_url_classes = True

-	if len( focus_labels_and_urls ) > 0 or len( selected_media_url_matches ) > 0 or multiple_or_unmatching_selection_url_matches:
+	if len( focus_labels_and_urls ) > 0 or len( selected_media_url_classes ) > 0 or multiple_or_unmatching_selection_url_classes:

		 urls_menu = wx.Menu()

@@ -387,16 +387,16 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):

		 # copy this file's urls

-		there_are_focus_url_matches_to_action = len( focus_matched_labels_and_urls ) > 1
-		multiple_or_unmatching_focus_url_matches = len( focus_unmatched_urls ) > 0 and len( focus_labels_and_urls ) > 1 # if there are unmatched urls and more than one thing total
+		there_are_focus_url_classes_to_action = len( focus_matched_labels_and_urls ) > 1
+		multiple_or_unmatching_focus_url_classes = len( focus_unmatched_urls ) > 0 and len( focus_labels_and_urls ) > 1 # if there are unmatched urls and more than one thing total

-		if there_are_focus_url_matches_to_action or multiple_or_unmatching_focus_url_matches:
+		if there_are_focus_url_classes_to_action or multiple_or_unmatching_focus_url_classes:

			 lib.GUI.Menus.AppendSeparator( urls_visit_menu )
			 lib.GUI.Menus.AppendSeparator( urls_copy_menu )

-		if there_are_focus_url_matches_to_action:
+		if there_are_focus_url_classes_to_action:

			 urls = [ url for ( label, url ) in focus_matched_labels_and_urls ]

@@ -411,7 +411,7 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):
			 lib.GUI.Menus.AppendMenuItem( win, urls_copy_menu, label, 'Copy these urls to your clipboard.', HG.client_controller.pub, 'clipboard', 'text', urls_string )

-		if multiple_or_unmatching_focus_url_matches:
+		if multiple_or_unmatching_focus_url_classes:

			 urls = [ url for ( label, url ) in focus_labels_and_urls ]

@@ -428,35 +428,35 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):

		 # now by url match type

-		there_are_selection_url_matches_to_action = len( selected_media_url_matches ) > 0
+		there_are_selection_url_classes_to_action = len( selected_media_url_classes ) > 0

-		if there_are_selection_url_matches_to_action or multiple_or_unmatching_selection_url_matches:
+		if there_are_selection_url_classes_to_action or multiple_or_unmatching_selection_url_classes:

			 lib.GUI.Menus.AppendSeparator( urls_visit_menu )
			 lib.GUI.Menus.AppendSeparator( urls_copy_menu )

-		if there_are_selection_url_matches_to_action:
+		if there_are_selection_url_classes_to_action:

-			selected_media_url_matches = list( selected_media_url_matches )
+			selected_media_url_classes = list( selected_media_url_classes )

-			selected_media_url_matches.sort( key = lambda url_match: url_match.GetName() )
+			selected_media_url_classes.sort( key = lambda url_class: url_class.GetName() )

-			for url_match in selected_media_url_matches:
+			for url_class in selected_media_url_classes:

-				label = 'open files\' ' + url_match.GetName() + ' urls in your web browser'
+				label = 'open files\' ' + url_class.GetName() + ' urls in your web browser'

-				lib.GUI.Menus.AppendMenuItem( win, urls_visit_menu, label, 'Open this url class in your web browser for all files.', OpenMediaURLMatchURLs, selected_media, url_match )
+				lib.GUI.Menus.AppendMenuItem( win, urls_visit_menu, label, 'Open this url class in your web browser for all files.', OpenMediaURLClassURLs, selected_media, url_class )

-				label = 'copy files\' ' + url_match.GetName() + ' urls'
+				label = 'copy files\' ' + url_class.GetName() + ' urls'

-				lib.GUI.Menus.AppendMenuItem( win, urls_copy_menu, label, 'Copy this url class for all files.', CopyMediaURLMatchURLs, selected_media, url_match )
+				lib.GUI.Menus.AppendMenuItem( win, urls_copy_menu, label, 'Copy this url class for all files.', CopyMediaURLClassURLs, selected_media, url_class )

		 # now everything

-		if multiple_or_unmatching_selection_url_matches:
+		if multiple_or_unmatching_selection_url_classes:

			 label = 'open all files\' urls'

diff --git a/include/ClientGUIPanels.py b/include/ClientGUIPanels.py
index d18e6ac..9e73159 100644
--- a/include/ClientGUIPanels.py
+++ b/include/ClientGUIPanels.py
@@ -25,6 +25,8 @@ class ReviewServicePanel( wx.Panel ):

		 wx.Panel.__init__( self, parent )

+		self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_FRAMEBK ) )
+
		 self._service = service

		 service_type = self._service.GetServiceType()
@@ -1189,29 +1191,27 @@ class ReviewServicePanel( wx.Panel ):

			 self._check_running_button = lib.GUI.Common.BetterButton( self, 'check daemon', self._CheckRunning )

-			self._ipfs_shares = lib.GUI.ListCtrl.SaneListCtrl( self, 200, [ ( 'multihash', 120 ), ( 'num files', 80 ), ( 'total size', 80 ), ( 'note', -1 ) ], delete_key_callback = self._Unpin, activation_callback = self._SetNotes )
+			self._ipfs_shares_panel = lib.GUI.ListCtrl.BetterListCtrlPanel( self )

-			self._copy_multihash_button = lib.GUI.Common.BetterButton( self, 'copy multihashes', self._CopyMultihashes )
-			self._show_selected_button = lib.GUI.Common.BetterButton( self, 'show selected in main gui', self._ShowSelectedInNewPages )
-			self._set_notes_button = lib.GUI.Common.BetterButton( self, 'set notes', self._SetNotes )
-			self._unpin_button = lib.GUI.Common.BetterButton( self, 'unpin selected', self._Unpin )
+			columns = [ ( 'multihash', 34 ), ( 'num files', 6 ), ( 'total size', 6 ), ( 'note', -1 ) ]

-			#
+			self._ipfs_shares = lib.GUI.ListCtrl.BetterListCtrl( self._ipfs_shares_panel, 'ipfs_shares', 12, 32, columns, self._ConvertDataToListCtrlTuple, delete_key_callback = self._Unpin, activation_callback = self._SetNotes )

-			self._Refresh()
+			self._ipfs_shares_panel.SetListCtrl( self._ipfs_shares )
+
+			self._ipfs_shares_panel.AddButton( 'copy multihashes', self._CopyMultihashes, enabled_only_on_selection = True )
+			self._ipfs_shares_panel.AddButton( 'show selected in main gui', self._ShowSelectedInNewPages, enabled_only_on_selection = True )
+			self._ipfs_shares_panel.AddButton( 'set notes', self._SetNotes, enabled_only_on_selection = True )
+			self._ipfs_shares_panel.AddButton( 'unpin selected', self._Unpin, enabled_only_on_selection = True )

			 #

-			button_box = wx.BoxSizer( wx.HORIZONTAL )
+			self._Refresh()

-			button_box.Add( self._copy_multihash_button, CC.FLAGS_VCENTER )
-			button_box.Add( self._show_selected_button, CC.FLAGS_VCENTER )
-			button_box.Add( self._set_notes_button, CC.FLAGS_VCENTER )
-			button_box.Add( self._unpin_button, CC.FLAGS_VCENTER )
+			#

			 self.Add( self._check_running_button, CC.FLAGS_LONE_BUTTON )
-			self.Add( self._ipfs_shares, CC.FLAGS_EXPAND_BOTH_WAYS )
-			self.Add( button_box, CC.FLAGS_BUTTON_SIZER )
+			self.Add( self._ipfs_shares_panel, CC.FLAGS_EXPAND_BOTH_WAYS )

			 HG.client_controller.sub( self, 'ServiceUpdated', 'service_updated' )

@@ -1253,13 +1253,28 @@ class ReviewServicePanel( wx.Panel ):
			 HG.client_controller.CallToThread( do_it )

+		def _ConvertDataToListCtrlTuple( self, data ):
+
+			( multihash, num_files, total_size, note ) = data
+
+			pretty_multihash = multihash
+			pretty_num_files = lib.Hydrus.Data.ToHumanInt( num_files )
+			pretty_total_size = lib.Hydrus.Data.ToHumanBytes( total_size )
+			pretty_note = note
+
+			display_tuple = ( pretty_multihash, pretty_num_files, pretty_total_size, pretty_note )
+			sort_tuple = ( multihash, num_files, total_size, note )
+
+			return ( display_tuple, sort_tuple )
+
+
		 def _CopyMultihashes( self ):

-			multihashes = [ multihash for ( multihash, num_files, total_size, note ) in self._ipfs_shares.GetSelectedClientData() ]
+			multihashes = [ multihash for ( multihash, num_files, total_size, note ) in self._ipfs_shares.GetData( only_selected = True ) ]

			 if len( multihashes ) == 0:

-				multihashes = [ multihash for ( multihash, num_files, total_size, note ) in self._ipfs_shares.GetClientData() ]
+				multihashes = [ multihash for ( multihash, num_files, total_size, note ) in self._ipfs_shares.GetData() ]

			 if len( multihashes ) > 0:
@@ -1272,18 +1287,6 @@ class ReviewServicePanel( wx.Panel ):

-		def _GetDisplayTuple( self, sort_tuple ):
-
-			( multihash, num_files, total_size, note ) = sort_tuple
-
-			pretty_multihash = multihash
-			pretty_num_files = lib.Hydrus.Data.ToHumanInt( num_files )
-			pretty_total_size = lib.Hydrus.Data.ToHumanBytes( total_size )
-			pretty_note = note
-
-			return ( pretty_multihash, pretty_num_files, pretty_total_size, pretty_note )
-
-
		 def _Refresh( self ):

			 if not self:
@@ -1296,74 +1299,89 @@ class ReviewServicePanel( wx.Panel ):

		 def _SetNotes( self ):

-			for ( multihash, num_files, total_size, note ) in self._ipfs_shares.GetSelectedClientData():
+			datas = self._ipfs_shares.GetData( only_selected = True )
+
+			if len( datas ) > 0:

-				with lib.GUI.Dialogs.DialogTextEntry( self, 'Set a note for ' + multihash + '.' ) as dlg:
+				with lib.GUI.Dialogs.DialogTextEntry( self, 'Set a note for these shares.' ) as dlg:

					 if dlg.ShowModal() == wx.ID_OK:

-						hashes = HG.client_controller.Read( 'service_directory', self._service.GetServiceKey(), multihash )
-
						 note = dlg.GetValue()

-						content_update_row = ( hashes, multihash, note )
+						content_updates = []

-						content_updates = [ lib.Hydrus.Data.ContentUpdate( HC.CONTENT_TYPE_DIRECTORIES, HC.CONTENT_UPDATE_ADD, content_update_row ) ]
+						for ( multihash, num_files, total_size, old_note ) in datas:
+
+							hashes = HG.client_controller.Read( 'service_directory', self._service.GetServiceKey(), multihash )
+
+							content_update_row = ( hashes, multihash, note )
+
+							content_updates.append( lib.Hydrus.Data.ContentUpdate( HC.CONTENT_TYPE_DIRECTORIES, HC.CONTENT_UPDATE_ADD, content_update_row ) )
+

						 HG.client_controller.Write( 'content_updates', { self._service.GetServiceKey() : content_updates } )

-					else:
-
-						break
+						self._my_updater.Update()

-			self._my_updater.Update()
-

		 def _ShowSelectedInNewPages( self ):

-			def do_it( shares ):
+			def wx_done():
+
+				self._ipfs_shares_panel.Enable()
+
+
+			def do_it( service_key, pages_of_hashes_to_show ):

				 try:

					 for ( multihash, num_files, total_size, note ) in shares:

-						hashes = HG.client_controller.Read( 'service_directory', self._service.GetServiceKey(), multihash )
+						hashes = HG.client_controller.Read( 'service_directory', service_key, multihash )

						 HG.client_controller.pub( 'new_page_query', CC.LOCAL_FILE_SERVICE_KEY, initial_hashes = hashes, page_name = 'ipfs directory' )

+						time.sleep( 0.5 )
+

				 finally:

-					wx.CallAfter( self._ipfs_shares.Enable )
+					wx.CallAfter( wx_done )

-			shares = self._ipfs_shares.GetSelectedClientData()
+			shares = self._ipfs_shares.GetData( only_selected = True )

-			self._ipfs_shares.Disable()
+			self._ipfs_shares_panel.Disable()

-			HG.client_controller.CallToThread( do_it, shares )
+			HG.client_controller.CallToThread( do_it, self._service.GetServiceKey(), shares )

		 def _Unpin( self ):

-			def do_it( multihashes ):
+			def wx_done():
+
+				self._ipfs_shares_panel.Enable()
+
+				self._my_updater.Update()
+
+
+			def do_it( service, multihashes ):

				 try:

-					for ( multihash, num_files, total_size, note ) in self._ipfs_shares.GetSelectedClientData():
+					for multihash in multihashes:

-						self._service.UnpinDirectory( multihash )
+						service.UnpinDirectory( multihash )

-					self._ipfs_shares.RemoveAllSelected()
-
				 finally:

-					wx.CallAfter( self._ipfs_shares.Enable )
+					wx.CallAfter( wx_done )

@@ -1371,11 +1389,11 @@ class ReviewServicePanel( wx.Panel ):

				 if dlg.ShowModal() == wx.ID_YES:

-					multihashes = [ multihash for ( multihash, num_files, total_size, note ) in self._ipfs_shares.GetSelectedClientData() ]
+					multihashes = [ multihash for ( multihash, num_files, total_size, note ) in self._ipfs_shares.GetData( only_selected = True ) ]

-					self._ipfs_shares.Disable()
+					self._ipfs_shares_panel.Disable()

-					HG.client_controller.CallToThread( do_it, multihashes )
+					HG.client_controller.CallToThread( do_it, self._service, multihashes )

@@ -1399,16 +1417,9 @@ class ReviewServicePanel( wx.Panel ):
					 return

-				self._ipfs_shares.DeleteAllItems()
+				# list of ( multihash, num_files, total_size, note )

-				for ( multihash, num_files, total_size, note ) in ipfs_shares:
-
-					sort_tuple = ( multihash, num_files, total_size, note )
-
-					display_tuple = self._GetDisplayTuple( sort_tuple )
-
-					self._ipfs_shares.Append( display_tuple, sort_tuple )
-
+				self._ipfs_shares.SetData( ipfs_shares )

			 ipfs_shares = HG.client_controller.Read( 'service_directories', service.GetServiceKey() )
diff --git a/include/lib.GUI.Parsing.py b/include/lib.GUI.Parsing.py
index ec80f39..cfb43c4 100644
--- a/include/lib.GUI.Parsing.py
+++ b/include/lib.GUI.Parsing.py
@@ -65,7 +65,7 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):
		 listctrl_panel.SetListCtrl( self._listctrl )

		 listctrl_panel.AddButton( 'add gug', self._AddGUG )
-		listctrl_panel.AddButton( 'add url class', self._AddURLMatch )
+		listctrl_panel.AddButton( 'add url class', self._AddURLClass )
		 listctrl_panel.AddButton( 'add parser', self._AddParser )
		 listctrl_panel.AddButton( 'add login script', self._AddLoginScript )
		 listctrl_panel.AddButton( 'add headers/bandwidth rules', self._AddDomainMetadata )
@@ -141,15 +141,15 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):

		 domain_metadatas_to_include = self._GetDomainMetadatasToInclude( domains )

-		url_matches_to_include = self._GetURLMatchesToInclude( gugs_to_include )
+		url_classes_to_include = self._GetURLClassesToInclude( gugs_to_include )

-		url_matches_to_include = self._FleshOutURLMatchesWithAPILinks( url_matches_to_include )
+		url_classes_to_include = self._FleshOutURLClassesWithAPILinks( url_classes_to_include )

-		parsers_to_include = self._GetParsersToInclude( url_matches_to_include )
+		parsers_to_include = self._GetParsersToInclude( url_classes_to_include )

		 self._listctrl.AddDatas( domain_metadatas_to_include )
		 self._listctrl.AddDatas( gugs_to_include )
-		self._listctrl.AddDatas( url_matches_to_include )
+		self._listctrl.AddDatas( url_classes_to_include )
		 self._listctrl.AddDatas( parsers_to_include )

@@ -207,13 +207,13 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):
		 self._listctrl.AddDatas( parsers_to_include )

-	def _AddURLMatch( self ):
+	def _AddURLClass( self ):

		 existing_data = self._listctrl.GetData()

-		choosable_url_matches = [ u for u in self._network_engine.domain_manager.GetURLMatches() if u not in existing_data ]
+		choosable_url_classes = [ u for u in self._network_engine.domain_manager.GetURLClasses() if u not in existing_data ]

-		choice_tuples = [ ( url_match.GetName(), url_match, False ) for url_match in choosable_url_matches ]
+		choice_tuples = [ ( url_class.GetName(), url_class, False ) for url_class in choosable_url_classes ]

		 with lib.GUI.TopLevelWindows.DialogEdit( self, 'select url classes' ) as dlg:

@@ -223,7 +223,7 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):

			 if dlg.ShowModal() == wx.ID_OK:

-				url_matches_to_include = panel.GetValue()
+				url_classes_to_include = panel.GetValue()

			 else:

@@ -231,11 +231,11 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):

-		url_matches_to_include = self._FleshOutURLMatchesWithAPILinks( url_matches_to_include )
+		url_classes_to_include = self._FleshOutURLClassesWithAPILinks( url_classes_to_include )

-		parsers_to_include = self._GetParsersToInclude( url_matches_to_include )
+		parsers_to_include = self._GetParsersToInclude( url_classes_to_include )

-		self._listctrl.AddDatas( url_matches_to_include )
+		self._listctrl.AddDatas( url_classes_to_include )
		 self._listctrl.AddDatas( parsers_to_include )

@@ -354,31 +354,31 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):

-	def _FleshOutURLMatchesWithAPILinks( self, url_matches ):
+	def _FleshOutURLClassesWithAPILinks( self, url_classes ):

-		url_matches_to_include = set( url_matches )
+		url_classes_to_include = set( url_classes )

-		api_links_dict = dict( lib.Networking.Domain.ConvertURLMatchesIntoAPIPairs( self._network_engine.domain_manager.GetURLMatches() ) )
+		api_links_dict = dict( lib.Networking.Domain.ConvertURLClassesIntoAPIPairs( self._network_engine.domain_manager.GetURLClasses() ) )

-		for url_match in url_matches:
+		for url_class in url_classes:

			 added_this_cycle = set()

-			while url_match in api_links_dict and url_match not in added_this_cycle:
+			while url_class in api_links_dict and url_class not in added_this_cycle:

-				added_this_cycle.add( url_match )
+				added_this_cycle.add( url_class )

-				url_match = api_links_dict[ url_match ]
+				url_class = api_links_dict[ url_class ]

-				url_matches_to_include.add( url_match )
+				url_classes_to_include.add( url_class )

		 existing_data = self._listctrl.GetData()

-		url_matches_to_include = [ u for u in url_matches_to_include if u not in existing_data ]
+		url_classes_to_include = [ u for u in url_classes_to_include if u not in existing_data ]

-		return url_matches_to_include
+		return url_classes_to_include

	 def _GetDomainMetadatasToInclude( self, domains ):
@@ -433,13 +433,13 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):
		 return domain_metadatas

-	def _GetParsersToInclude( self, url_matches ):
+	def _GetParsersToInclude( self, url_classes ):

		 parsers_to_include = set()

-		for url_match in url_matches:
+		for url_class in url_classes:

-			example_url = url_match.GetExampleURL()
+			example_url = url_class.GetExampleURL()

			 ( url_type, match_name, can_parse ) = self._network_engine.domain_manager.GetURLParseCapability( example_url )

@@ -463,9 +463,9 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):
		 return [ p for p in parsers_to_include if p not in existing_data ]

-	def _GetURLMatchesToInclude( self, gugs ):
+	def _GetURLClassesToInclude( self, gugs ):

-		url_matches_to_include = set()
+		url_classes_to_include = set()

		 for gug in gugs:

@@ -480,21 +480,21 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):

			 for example_url in example_urls:

-				url_match = self._network_engine.domain_manager.GetURLMatch( example_url )
+				url_class = self._network_engine.domain_manager.GetURLClass( example_url )

-				if url_match is not None:
+				if url_class is not None:

-					url_matches_to_include.add( url_match )
+					url_classes_to_include.add( url_class )

					 # add post url matches from same domain

					 domain = lib.Networking.Domain.ConvertURLIntoSecondLevelDomain( example_url )

-					for um in list( self._network_engine.domain_manager.GetURLMatches() ):
+					for um in list( self._network_engine.domain_manager.GetURLClasses() ):

						 if lib.Networking.Domain.ConvertURLIntoSecondLevelDomain( um.GetExampleURL() ) == domain and um.GetURLType() in ( HC.URL_TYPE_POST, HC.URL_TYPE_FILE ):

-							url_matches_to_include.add( um )
+							url_classes_to_include.add( um )

@@ -503,7 +503,7 @@ class DownloaderExportPanel( lib.GUI.ScrolledPanels.ReviewPanel ):

		 existing_data = self._listctrl.GetData()

-		return [ u for u in url_matches_to_include if u not in existing_data ]
+		return [ u for u in url_classes_to_include if u not in existing_data ]

 class EditCompoundFormulaPanel( lib.GUI.ScrolledPanels.EditPanel ):
diff --git a/include/lib.GUI.Predicates.py b/include/lib.GUI.Predicates.py
index be30bed..5b4d787 100644
--- a/include/lib.GUI.Predicates.py
+++ b/include/lib.GUI.Predicates.py
@@ -53,7 +53,7 @@ class InputFileSystemPredicate( lib.GUI.ScrolledPanels.EditPanel ):
			 pred_classes.append( PanelPredicateSystemKnownURLsExactURL )
			 pred_classes.append( PanelPredicateSystemKnownURLsDomain )
			 pred_classes.append( PanelPredicateSystemKnownURLsRegex )
-			pred_classes.append( PanelPredicateSystemKnownURLsURLMatch )
+			pred_classes.append( PanelPredicateSystemKnownURLsURLClass )

		 elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_HASH:

@@ -777,7 +777,7 @@ class PanelPredicateSystemKnownURLsRegex( PanelPredicateSystem ):
		 return ( operator, rule_type, rule, description )

-class PanelPredicateSystemKnownURLsURLMatch( PanelPredicateSystem ):
+class PanelPredicateSystemKnownURLsURLClass( PanelPredicateSystem ):

	 PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS

@@ -790,13 +790,13 @@ class PanelPredicateSystemKnownURLsURLMatch( PanelPredicateSystem ):
		 self._operator.Append( 'has', True )
		 self._operator.Append( 'does not have', False )

-		self._url_matches = lib.GUI.Common.BetterChoice( self )
+		self._url_classes = lib.GUI.Common.BetterChoice( self )

-		for url_match in HG.client_controller.network_engine.domain_manager.GetURLMatches():
+		for url_class in HG.client_controller.network_engine.domain_manager.GetURLClasses():

-			if url_match.ShouldAssociateWithFiles():
+			if url_class.ShouldAssociateWithFiles():

-				self._url_matches.Append( url_match.GetName(), url_match )
+				self._url_classes.Append( url_class.GetName(), url_class )

@@ -805,7 +805,7 @@ class PanelPredicateSystemKnownURLsURLMatch( PanelPredicateSystem ):
		 hbox.Add( lib.GUI.Common.BetterStaticText( self, 'system:known url' ), CC.FLAGS_VCENTER )
		 hbox.Add( self._operator, CC.FLAGS_VCENTER )
		 hbox.Add( lib.GUI.Common.BetterStaticText( self, 'url matching this class:' ), CC.FLAGS_VCENTER )
-		hbox.Add( self._url_matches, CC.FLAGS_VCENTER )
+		hbox.Add( self._url_classes, CC.FLAGS_VCENTER )

		 self.SetSizer( hbox )

@@ -823,13 +823,13 @@ class PanelPredicateSystemKnownURLsURLMatch( PanelPredicateSystem ):
			 operator_description = 'does not have '

-		rule_type = 'url_match'
+		rule_type = 'url_class'

-		url_match = self._url_matches.GetChoice()
+		url_class = self._url_classes.GetChoice()

-		rule = url_match
+		rule = url_class

-		description = operator_description + url_match.GetName() + ' url'
+		description = operator_description + url_class.GetName() + ' url'

		 return ( operator, rule_type, rule, description )

diff --git a/include/lib.GUI.ScrolledPanels.Edit.py b/include/lib.GUI.ScrolledPanels.Edit.py
index 309335e..da941d1 100644
--- a/include/lib.GUI.ScrolledPanels.Edit.py
+++ b/include/lib.GUI.ScrolledPanels.Edit.py
@@ -313,16 +313,16 @@ class EditCookiePanel( lib.GUI.ScrolledPanels.EditPanel ):

 class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

-	def __init__( self, parent, url_matches, parsers, url_match_keys_to_parser_keys, file_post_default_tag_import_options, watchable_default_tag_import_options, url_match_keys_to_tag_import_options ):
+	def __init__( self, parent, url_classes, parsers, url_class_keys_to_parser_keys, file_post_default_tag_import_options, watchable_default_tag_import_options, url_class_keys_to_tag_import_options ):

		 lib.GUI.ScrolledPanels.EditPanel.__init__( self, parent )

-		self._url_matches = url_matches
+		self._url_classes = url_classes
		 self._parsers = parsers
-		self._url_match_keys_to_parser_keys = url_match_keys_to_parser_keys
+		self._url_class_keys_to_parser_keys = url_class_keys_to_parser_keys
		 self._parser_keys_to_parsers = { parser.GetParserKey() : parser for parser in self._parsers }

-		self._url_match_keys_to_tag_import_options = dict( url_match_keys_to_tag_import_options )
+		self._url_class_keys_to_tag_import_options = dict( url_class_keys_to_tag_import_options )

		 #

@@ -346,9 +346,9 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 #

-		eligible_url_matches = [ url_match for url_match in url_matches if url_match.GetURLType() in ( HC.URL_TYPE_POST, HC.URL_TYPE_WATCHABLE ) and url_match.GetMatchKey() in self._url_match_keys_to_parser_keys ]
+		eligible_url_classes = [ url_class for url_class in url_classes if url_class.GetURLType() in ( HC.URL_TYPE_POST, HC.URL_TYPE_WATCHABLE ) and url_class.GetMatchKey() in self._url_class_keys_to_parser_keys ]

-		self._list_ctrl.AddDatas( eligible_url_matches )
+		self._list_ctrl.AddDatas( eligible_url_classes )

		 self._list_ctrl.Sort( 1 )

@@ -369,13 +369,13 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):
		 self.SetSizer( vbox )

-	def _ConvertDataToListCtrlTuples( self, url_match ):
+	def _ConvertDataToListCtrlTuples( self, url_class ):

-		url_match_key = url_match.GetMatchKey()
+		url_class_key = url_class.GetMatchKey()

-		name = url_match.GetName()
-		url_type = url_match.GetURLType()
-		defaults_set = url_match_key in self._url_match_keys_to_tag_import_options
+		name = url_class.GetName()
+		url_type = url_class.GetURLType()
+		defaults_set = url_class_key in self._url_class_keys_to_tag_import_options

		 pretty_name = name
		 pretty_url_type = HC.url_type_string_lookup[ url_type ]
@@ -401,19 +401,19 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

			 if dlg.ShowModal() == wx.ID_YES:

-				url_matches_to_clear = self._list_ctrl.GetData( only_selected = True )
+				url_classes_to_clear = self._list_ctrl.GetData( only_selected = True )

-				for url_match in url_matches_to_clear:
+				for url_class in url_classes_to_clear:

-					url_match_key = url_match.GetMatchKey()
+					url_class_key = url_class.GetMatchKey()

-					if url_match_key in self._url_match_keys_to_tag_import_options:
+					if url_class_key in self._url_class_keys_to_tag_import_options:

-						del self._url_match_keys_to_tag_import_options[ url_match_key ]
+						del self._url_class_keys_to_tag_import_options[ url_class_key ]

-				self._list_ctrl.UpdateDatas( url_matches_to_clear )
+				self._list_ctrl.UpdateDatas( url_classes_to_clear )

@@ -424,13 +424,13 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 if len( selected ) == 1:

-			url_match = selected[0]
+			url_class = selected[0]

-			url_match_key = url_match.GetMatchKey()
+			url_class_key = url_class.GetMatchKey()

-			if url_match_key in self._url_match_keys_to_tag_import_options:
+			if url_class_key in self._url_class_keys_to_tag_import_options:

-				tag_import_options = self._url_match_keys_to_tag_import_options[ url_match_key ]
+				tag_import_options = self._url_class_keys_to_tag_import_options[ url_class_key ]

				 json_string = tag_import_options.DumpToString()

@@ -441,13 +441,13 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def _Edit( self ):

-		url_matches_to_edit = self._list_ctrl.GetData( only_selected = True )
+		url_classes_to_edit = self._list_ctrl.GetData( only_selected = True )

-		for url_match in url_matches_to_edit:
+		for url_class in url_classes_to_edit:

			 with lib.GUI.TopLevelWindows.DialogEdit( self, 'edit tag import options' ) as dlg:

-				tag_import_options = self._GetDefaultTagImportOptions( url_match )
+				tag_import_options = self._GetDefaultTagImportOptions( url_class )
				 show_downloader_options = True

				 panel = EditTagImportOptionsPanel( dlg, tag_import_options, show_downloader_options )
@@ -456,11 +456,11 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

				 if dlg.ShowModal() == wx.ID_OK:

-					url_match_key = url_match.GetMatchKey()
+					url_class_key = url_class.GetMatchKey()

					 tag_import_options = panel.GetValue()

-					self._url_match_keys_to_tag_import_options[ url_match_key ] = tag_import_options
+					self._url_class_keys_to_tag_import_options[ url_class_key ] = tag_import_options

				 else:

@@ -469,20 +469,20 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

-		self._list_ctrl.UpdateDatas( url_matches_to_edit )
+		self._list_ctrl.UpdateDatas( url_classes_to_edit )

-	def _GetDefaultTagImportOptions( self, url_match ):
+	def _GetDefaultTagImportOptions( self, url_class ):

-		url_match_key = url_match.GetMatchKey()
+		url_class_key = url_class.GetMatchKey()

-		if url_match_key in self._url_match_keys_to_tag_import_options:
+		if url_class_key in self._url_class_keys_to_tag_import_options:

-			tag_import_options = self._url_match_keys_to_tag_import_options[ url_match_key ]
+			tag_import_options = self._url_class_keys_to_tag_import_options[ url_class_key ]

		 else:

-			url_type = url_match.GetURLType()
+			url_type = url_class.GetURLType()

			 if url_type == HC.URL_TYPE_POST:

@@ -494,7 +494,7 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

			 else:

-				raise lib.Hydrus.Exceptions.URLMatchException( 'Could not find tag import options for that kind of URL Class!' )
+				raise lib.Hydrus.Exceptions.URLClassException( 'Could not find tag import options for that kind of URL Class!' )

@@ -507,11 +507,11 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 if len( selected ) == 1:

-			url_match = selected[0]
+			url_class = selected[0]

-			url_match_key = url_match.GetMatchKey()
+			url_class_key = url_class.GetMatchKey()

-			if url_match_key in self._url_match_keys_to_tag_import_options:
+			if url_class_key in self._url_class_keys_to_tag_import_options:

				 return True

@@ -533,11 +533,11 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):
				 raise Exception( 'Not a Tag Import Options!' )

-			for url_match in self._list_ctrl.GetData( only_selected = True ):
+			for url_class in self._list_ctrl.GetData( only_selected = True ):

-				url_match_key = url_match.GetMatchKey()
+				url_class_key = url_class.GetMatchKey()

-				self._url_match_keys_to_tag_import_options[ url_match_key ] = tag_import_options.Duplicate()
+				self._url_class_keys_to_tag_import_options[ url_class_key ] = tag_import_options.Duplicate()

			 self._list_ctrl.UpdateDatas()
@@ -555,7 +555,7 @@ class EditDefaultTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):
		 file_post_default_tag_import_options = self._file_post_default_tag_import_options_button.GetValue()
		 watchable_default_tag_import_options = self._watchable_default_tag_import_options_button.GetValue()

-		return ( file_post_default_tag_import_options, watchable_default_tag_import_options, self._url_match_keys_to_tag_import_options )
+		return ( file_post_default_tag_import_options, watchable_default_tag_import_options, self._url_class_keys_to_tag_import_options )

 class EditDeleteFilesPanel( lib.GUI.ScrolledPanels.EditPanel ):
@@ -855,16 +855,16 @@ class EditDeleteFilesPanel( lib.GUI.ScrolledPanels.EditPanel ):

 class EditDomainManagerInfoPanel( lib.GUI.ScrolledPanels.EditPanel ):

-	def __init__( self, parent, url_matches, network_contexts_to_custom_header_dicts ):
+	def __init__( self, parent, url_classes, network_contexts_to_custom_header_dicts ):

		 lib.GUI.ScrolledPanels.EditPanel.__init__( self, parent )

		 self._notebook = wx.Notebook( self )

-		self._url_matches_panel = EditURLMatchesPanel( self._notebook, url_matches )
+		self._url_classes_panel = EditURLClassesPanel( self._notebook, url_classes )
		 self._network_contexts_to_custom_header_dicts_panel = EditNetworkContextCustomHeadersPanel( self._notebook, network_contexts_to_custom_header_dicts )

-		self._notebook.AddPage( self._url_matches_panel, 'url classes', select = True )
+		self._notebook.AddPage( self._url_classes_panel, 'url classes', select = True )
		 self._notebook.AddPage( self._network_contexts_to_custom_header_dicts_panel, 'custom headers', select = False )

		 vbox = wx.BoxSizer( wx.VERTICAL )
@@ -876,23 +876,23 @@ class EditDomainManagerInfoPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def GetValue( self ):

-		url_matches = self._url_matches_panel.GetValue()
+		url_classes = self._url_classes_panel.GetValue()
		 network_contexts_to_custom_header_dicts = self._network_contexts_to_custom_header_dicts_panel.GetValue()

-		return ( url_matches, network_contexts_to_custom_header_dicts )
+		return ( url_classes, network_contexts_to_custom_header_dicts )

 class EditDownloaderDisplayPanel( lib.GUI.ScrolledPanels.EditPanel ):

-	def __init__( self, parent, network_engine, gugs, gug_keys_to_display, url_matches, url_match_keys_to_display ):
+	def __init__( self, parent, network_engine, gugs, gug_keys_to_display, url_classes, url_class_keys_to_display ):

		 lib.GUI.ScrolledPanels.EditPanel.__init__( self, parent )

		 self._gugs = gugs
		 self._gug_keys_to_gugs = { gug.GetGUGKey() : gug for gug in self._gugs }

-		self._url_matches = url_matches
-		self._url_match_keys_to_url_matches = { url_match.GetMatchKey() : url_match for url_match in self._url_matches }
+		self._url_classes = url_classes
+		self._url_class_keys_to_url_classes = { url_class.GetMatchKey() : url_class for url_class in self._url_classes }

		 self._network_engine = network_engine

@@ -918,7 +918,7 @@ class EditDownloaderDisplayPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 columns = [ ( 'url class', -1 ), ( 'url type', 20 ), ( 'display on media viewer?', 36 ) ]

-		self._url_display_list_ctrl = lib.GUI.ListCtrl.BetterListCtrl( self._url_display_list_ctrl_panel, 'url_match_keys_to_display', 15, 36, columns, self._ConvertURLDisplayDataToListCtrlTuples, activation_callback = self._EditURLDisplay )
+		self._url_display_list_ctrl = lib.GUI.ListCtrl.BetterListCtrl( self._url_display_list_ctrl_panel, 'url_class_keys_to_display', 15, 36, columns, self._ConvertURLDisplayDataToListCtrlTuples, activation_callback = self._EditURLDisplay )

		 self._url_display_list_ctrl_panel.SetListCtrl( self._url_display_list_ctrl )

@@ -943,11 +943,11 @@ class EditDownloaderDisplayPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 listctrl_data = []

-		for ( url_match_key, url_match ) in list(self._url_match_keys_to_url_matches.items()):
+		for ( url_class_key, url_class ) in list(self._url_class_keys_to_url_classes.items()):

-			display = url_match_key in url_match_keys_to_display
+			display = url_class_key in url_class_keys_to_display

-			listctrl_data.append( ( url_match_key, display ) )
+			listctrl_data.append( ( url_class_key, display ) )

		 self._url_display_list_ctrl.AddDatas( listctrl_data )
@@ -995,14 +995,14 @@ class EditDownloaderDisplayPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def _ConvertURLDisplayDataToListCtrlTuples( self, data ):

-		( url_match_key, display ) = data
+		( url_class_key, display ) = data

-		url_match = self._url_match_keys_to_url_matches[ url_match_key ]
+		url_class = self._url_class_keys_to_url_classes[ url_class_key ]

-		url_match_name = url_match.GetName()
-		url_type = url_match.GetURLType()
+		url_class_name = url_class.GetName()
+		url_type = url_class.GetURLType()

-		pretty_name = url_match_name
+		pretty_name = url_class_name
		 pretty_url_type = HC.url_type_string_lookup[ url_type ]

		 if display:
@@ -1015,7 +1015,7 @@ class EditDownloaderDisplayPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 display_tuple = ( pretty_name, pretty_url_type, pretty_display )
-		sort_tuple = ( url_match_name, pretty_url_type, display )
+		sort_tuple = ( url_class_name, pretty_url_type, display )

		 return ( display_tuple, sort_tuple )

@@ -1058,11 +1058,11 @@ class EditDownloaderDisplayPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 for data in self._url_display_list_ctrl.GetData( only_selected = True ):

-			( url_match_key, display ) = data
+			( url_class_key, display ) = data

-			url_match_name = self._url_match_keys_to_url_matches[ url_match_key ].GetName()
+			url_class_name = self._url_class_keys_to_url_classes[ url_class_key ].GetName()

-			message = 'Show ' + url_match_name + ' in the media viewer?'
+			message = 'Show ' + url_class_name + ' in the media viewer?'

			 with lib.GUI.Dialogs.DialogYesNo( self, message, title = 'Show in the media viewer?' ) as dlg:

@@ -1074,7 +1074,7 @@ class EditDownloaderDisplayPanel( lib.GUI.ScrolledPanels.EditPanel ):

					 self._url_display_list_ctrl.DeleteDatas( ( data, ) )

-					new_data = ( url_match_key, display )
+					new_data = ( url_class_key, display )

					 self._url_display_list_ctrl.AddDatas( ( new_data, ) )

@@ -1091,9 +1091,9 @@ class EditDownloaderDisplayPanel( lib.GUI.ScrolledPanels.EditPanel ):
	 def GetValue( self ):

		 gug_keys_to_display = { gug_key for ( gug_key, display ) in self._gug_display_list_ctrl.GetData() if display }
-		url_match_keys_to_display = { url_match_key for ( url_match_key, display ) in self._url_display_list_ctrl.GetData() if display }
+		url_class_keys_to_display = { url_class_key for ( url_class_key, display ) in self._url_display_list_ctrl.GetData() if display }

-		return ( gug_keys_to_display, url_match_keys_to_display )
+		return ( gug_keys_to_display, url_class_keys_to_display )

 class EditDuplicateActionOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):
@@ -1920,7 +1920,7 @@ class EditGUGPanel( lib.GUI.ScrolledPanels.EditPanel ):
		 self._example_search_text = wx.TextCtrl( self )

		 self._example_url = wx.TextCtrl( self, style = wx.TE_READONLY )
-		self._matched_url_match = wx.TextCtrl( self, style = wx.TE_READONLY )
+		self._matched_url_class = wx.TextCtrl( self, style = wx.TE_READONLY )

		 #

@@ -1950,7 +1950,7 @@ class EditGUGPanel( lib.GUI.ScrolledPanels.EditPanel ):
		 rows.append( ( 'initial search text (to prompt user): ', self._initial_search_text ) )
		 rows.append( ( 'example search text: ', self._example_search_text ) )
		 rows.append( ( 'example url: ', self._example_url ) )
-		rows.append( ( 'matches as a: ', self._matched_url_match ) )
+		rows.append( ( 'matches as a: ', self._matched_url_class ) )

		 gridbox = lib.GUI.Common.WrapInGrid( self, rows )

@@ -1995,7 +1995,7 @@ class EditGUGPanel( lib.GUI.ScrolledPanels.EditPanel ):

			 self._example_url.SetValue( example_url )

-		except ( lib.Hydrus.Exceptions.GUGException, lib.Hydrus.Exceptions.URLMatchException ) as e:
+		except ( lib.Hydrus.Exceptions.GUGException, lib.Hydrus.Exceptions.URLClassException ) as e:

			 reason = str( e )

@@ -2006,22 +2006,22 @@ class EditGUGPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 if example_url is None:

-			self._matched_url_match.SetValue( '' )
+			self._matched_url_class.SetValue( '' )

		 else:

-			url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( example_url )
+			url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( example_url )

-			if url_match is None:
+			if url_class is None:

-				url_match_text = 'Did not match a known url class.'
+				url_class_text = 'Did not match a known url class.'

			 else:

-				url_match_text = 'Matched ' + url_match.GetName() + ' url class.'
+				url_class_text = 'Matched ' + url_class.GetName() + ' url class.'

-			self._matched_url_match.SetValue( url_match_text )
+			self._matched_url_class.SetValue( url_class_text )

@@ -2340,30 +2340,30 @@ class EditGUGsPanel( lib.GUI.ScrolledPanels.EditPanel ):

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

-			url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( example_url )
+			url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( example_url )

		 except Exception:

			 example_url = 'unable to parse example url'
-			url_match = None
+			url_class = None

-		if url_match is None:
+		if url_class is None:

-			gallery_url_match = False
-			pretty_gallery_url_match = ''
+			gallery_url_class = False
+			pretty_gallery_url_class = ''

		 else:

-			gallery_url_match = True
-			pretty_gallery_url_match = url_match.GetName()
+			gallery_url_class = True
+			pretty_gallery_url_class = url_class.GetName()

		 pretty_name = name
		 pretty_example_url = example_url

-		display_tuple = ( pretty_name, pretty_example_url, pretty_gallery_url_match )
-		sort_tuple = ( name, example_url, gallery_url_match )
+		display_tuple = ( pretty_name, pretty_example_url, pretty_gallery_url_class )
+		sort_tuple = ( name, example_url, gallery_url_class )

		 return ( display_tuple, sort_tuple )

@@ -5260,26 +5260,26 @@ class EditTagImportOptionsPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 domain_manager = HG.client_controller.network_engine.domain_manager

-		( file_post_default_tag_import_options, watchable_default_tag_import_options, url_match_keys_to_default_tag_import_options ) = domain_manager.GetDefaultTagImportOptions()
+		( file_post_default_tag_import_options, watchable_default_tag_import_options, url_class_keys_to_default_tag_import_options ) = domain_manager.GetDefaultTagImportOptions()

		 choice_tuples = []

		 choice_tuples.append( ( 'file post default', file_post_default_tag_import_options ) )
		 choice_tuples.append( ( 'watchable default', watchable_default_tag_import_options ) )

-		if len( url_match_keys_to_default_tag_import_options ) > 0:
+		if len( url_class_keys_to_default_tag_import_options ) > 0:

			 choice_tuples.append( ( '----', None ) )

-			url_matches = domain_manager.GetURLMatches()
+			url_classes = domain_manager.GetURLClasses()

-			url_match_keys_to_url_matches = { url_match.GetMatchKey() : url_match for url_match in url_matches }
+			url_class_keys_to_url_classes = { url_class.GetMatchKey() : url_class for url_class in url_classes }

-			url_match_names_and_default_tag_import_options = [ ( url_match_keys_to_url_matches[ url_match_key ].GetName(), url_match_keys_to_default_tag_import_options[ url_match_key ] ) for url_match_key in list( url_match_keys_to_default_tag_import_options.keys() ) if url_match_key in url_match_keys_to_url_matches ]
+			url_class_names_and_default_tag_import_options = [ ( url_class_keys_to_url_classes[ url_class_key ].GetName(), url_class_keys_to_default_tag_import_options[ url_class_key ] ) for url_class_key in list( url_class_keys_to_default_tag_import_options.keys() ) if url_class_key in url_class_keys_to_url_classes ]

-			url_match_names_and_default_tag_import_options.sort()
+			url_class_names_and_default_tag_import_options.sort()

-			choice_tuples.extend( url_match_names_and_default_tag_import_options )
+			choice_tuples.extend( url_class_names_and_default_tag_import_options )

		 try:
@@ -5902,13 +5902,13 @@ class TagSummaryGeneratorButton( lib.GUI.Common.BetterButton ):
		 return self._tag_summary_generator

-class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):
+class EditURLClassPanel( lib.GUI.ScrolledPanels.EditPanel ):

-	def __init__( self, parent, url_match ):
+	def __init__( self, parent, url_class ):

		 lib.GUI.ScrolledPanels.EditPanel.__init__( self, parent )

-		self._original_url_match = url_match
+		self._original_url_class = url_class

		 self._name = wx.TextCtrl( self )

@@ -5976,7 +5976,7 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 columns = [ ( 'key', 14 ), ( 'value', -1 ) ]

-		self._parameters = lib.GUI.ListCtrl.BetterListCtrl( parameters_listctrl_panel, 'url_match_path_components', 5, 45, columns, self._ConvertParameterToListCtrlTuples, delete_key_callback = self._DeleteParameters, activation_callback = self._EditParameters )
+		self._parameters = lib.GUI.ListCtrl.BetterListCtrl( parameters_listctrl_panel, 'url_class_path_components', 5, 45, columns, self._ConvertParameterToListCtrlTuples, delete_key_callback = self._DeleteParameters, activation_callback = self._EditParameters )

		 parameters_listctrl_panel.SetListCtrl( self._parameters )

@@ -5996,7 +5996,7 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 self._example_url = wx.TextCtrl( self )

-		self._example_url_matches = lib.GUI.Common.BetterStaticText( self )
+		self._example_url_classes = lib.GUI.Common.BetterStaticText( self )

		 self._normalised_url = wx.TextCtrl( self, style = wx.TE_READONLY )

@@ -6006,7 +6006,7 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 self._normalised_url.SetToolTip( tt )

-		( url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, path_components, parameters, api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, example_url ) = url_match.ToTuple()
+		( url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, path_components, parameters, api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, example_url ) = url_class.ToTuple()

		 self._api_lookup_converter = lib.GUI.Controls.StringConverterButton( self, api_lookup_converter )

@@ -6016,7 +6016,7 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 #

-		name = url_match.GetName()
+		name = url_class.GetName()

		 self._name.SetValue( name )

@@ -6043,7 +6043,7 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 self._example_url.SetMinSize( ( example_url_width, -1 ) )

-		( gallery_index_type, gallery_index_identifier, gallery_index_delta ) = url_match.GetGalleryIndexValues()
+		( gallery_index_type, gallery_index_identifier, gallery_index_delta ) = url_class.GetGalleryIndexValues()

		 # this preps it for the upcoming update
		 self._next_gallery_page_choice.Append( 'initialisation', ( gallery_index_type, gallery_index_identifier ) )
@@ -6102,7 +6102,7 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):
		 vbox.Add( path_components_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
		 vbox.Add( parameters_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
		 vbox.Add( self._next_gallery_page_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
-		vbox.Add( self._example_url_matches, CC.FLAGS_EXPAND_PERPENDICULAR )
+		vbox.Add( self._example_url_classes, CC.FLAGS_EXPAND_PERPENDICULAR )
		 vbox.Add( gridbox_2, CC.FLAGS_EXPAND_PERPENDICULAR )

		 self.SetSizer( vbox )
@@ -6392,7 +6392,7 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def _GetValue( self ):

-		url_match_key = self._original_url_match.GetMatchKey()
+		url_class_key = self._original_url_class.GetMatchKey()
		 name = self._name.GetValue()
		 url_type = self._url_type.GetChoice()
		 preferred_scheme = self._preferred_scheme.GetChoice()
@@ -6410,9 +6410,9 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 example_url = self._example_url.GetValue()

-		url_match = lib.Networking.Domain.URLMatch( name, url_match_key = url_match_key, url_type = url_type, preferred_scheme = preferred_scheme, netloc = netloc, match_subdomains = match_subdomains, keep_matched_subdomains = keep_matched_subdomains, path_components = path_components, parameters = parameters, api_lookup_converter = api_lookup_converter, can_produce_multiple_files = can_produce_multiple_files, should_be_associated_with_files = should_be_associated_with_files, gallery_index_type = gallery_index_type, gallery_index_identifier = gallery_index_identifier, gallery_index_delta = gallery_index_delta, example_url = example_url )
+		url_class = lib.Networking.Domain.URLClass( name, url_class_key = url_class_key, url_type = url_type, preferred_scheme = preferred_scheme, netloc = netloc, match_subdomains = match_subdomains, keep_matched_subdomains = keep_matched_subdomains, path_components = path_components, parameters = parameters, api_lookup_converter = api_lookup_converter, can_produce_multiple_files = can_produce_multiple_files, should_be_associated_with_files = should_be_associated_with_files, gallery_index_type = gallery_index_type, gallery_index_identifier = gallery_index_identifier, gallery_index_delta = gallery_index_delta, example_url = example_url )

-		return url_match
+		return url_class

	 def _UpdateControls( self ):
@@ -6470,9 +6470,9 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 #

-		url_match = self._GetValue()
+		url_class = self._GetValue()

-		url_type = url_match.GetURLType()
+		url_type = url_class.GetURLType()

		 if url_type == HC.URL_TYPE_POST:

@@ -6483,7 +6483,7 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):
			 self._can_produce_multiple_files.Disable()

-		if url_match.ClippingIsAppropriate():
+		if url_class.ClippingIsAppropriate():

			 if self._match_subdomains.GetValue():

@@ -6506,20 +6506,20 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

			 self._api_lookup_converter.SetExampleString( example_url )

-			url_match.Test( example_url )
+			url_class.Test( example_url )

-			self._example_url_matches.SetLabelText( 'Example matches ok!' )
-			self._example_url_matches.SetForegroundColour( ( 0, 128, 0 ) )
+			self._example_url_classes.SetLabelText( 'Example matches ok!' )
+			self._example_url_classes.SetForegroundColour( ( 0, 128, 0 ) )

-			normalised = url_match.Normalise( example_url )
+			normalised = url_class.Normalise( example_url )

			 self._normalised_url.SetValue( normalised )

			 try:

-				if url_match.UsesAPIURL():
+				if url_class.UsesAPIURL():

-					api_lookup_url = url_match.GetAPIURL( normalised )
+					api_lookup_url = url_class.GetAPIURL( normalised )

				 else:

@@ -6537,9 +6537,9 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

			 try:

-				if url_match.CanGenerateNextGalleryPage():
+				if url_class.CanGenerateNextGalleryPage():

-					next_gallery_page_url = url_match.GetNextGalleryPage( normalised )
+					next_gallery_page_url = url_class.GetNextGalleryPage( normalised )

				 else:

@@ -6555,12 +6555,12 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):
				 self._next_gallery_page_url.SetValue( 'Could not convert - ' + reason )

-		except lib.Hydrus.Exceptions.URLMatchException as e:
+		except lib.Hydrus.Exceptions.URLClassException as e:

			 reason = str( e )

-			self._example_url_matches.SetLabelText( 'Example does not match - ' + reason )
-			self._example_url_matches.SetForegroundColour( ( 128, 0, 0 ) )
+			self._example_url_classes.SetLabelText( 'Example does not match - ' + reason )
+			self._example_url_classes.SetForegroundColour( ( 128, 0, 0 ) )

			 self._normalised_url.SetValue( '' )
			 self._api_url.SetValue( '' )
@@ -6618,23 +6618,23 @@ class EditURLMatchPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def GetValue( self ):

-		url_match = self._GetValue()
+		url_class = self._GetValue()

		 try:

-			url_match.Test( self._example_url.GetValue() )
+			url_class.Test( self._example_url.GetValue() )

-		except lib.Hydrus.Exceptions.URLMatchException:
+		except lib.Hydrus.Exceptions.URLClassException:

			 raise lib.Hydrus.Exceptions.VetoException( 'Please enter an example url that matches the given rules!' )

-		return url_match
+		return url_class

-class EditURLMatchesPanel( lib.GUI.ScrolledPanels.EditPanel ):
+class EditURLClassesPanel( lib.GUI.ScrolledPanels.EditPanel ):

-	def __init__( self, parent, url_matches ):
+	def __init__( self, parent, url_classes ):

		 lib.GUI.ScrolledPanels.EditPanel.__init__( self, parent )

@@ -6657,7 +6657,7 @@ class EditURLMatchesPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 columns = [ ( 'name', 36 ), ( 'type', 20 ), ( 'example (normalised) url', -1 ) ]

-		self._list_ctrl = lib.GUI.ListCtrl.BetterListCtrl( self._list_ctrl_panel, 'url_matches', 15, 40, columns, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+		self._list_ctrl = lib.GUI.ListCtrl.BetterListCtrl( self._list_ctrl_panel, 'url_classes', 15, 40, columns, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )

		 self._list_ctrl_panel.SetListCtrl( self._list_ctrl )

@@ -6665,13 +6665,13 @@ class EditURLMatchesPanel( lib.GUI.ScrolledPanels.EditPanel ):
		 self._list_ctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
		 self._list_ctrl_panel.AddDeleteButton()
		 self._list_ctrl_panel.AddSeparator()
-		self._list_ctrl_panel.AddImportExportButtons( ( lib.Networking.Domain.URLMatch, ), self._AddURLMatch )
+		self._list_ctrl_panel.AddImportExportButtons( ( lib.Networking.Domain.URLClass, ), self._AddURLClass )
		 self._list_ctrl_panel.AddSeparator()
-		self._list_ctrl_panel.AddDefaultsButton( lib.Hydrus.Defaults.GetDefaultURLMatches, self._AddURLMatch )
+		self._list_ctrl_panel.AddDefaultsButton( lib.Hydrus.Defaults.GetDefaultURLClasses, self._AddURLClass )

		 #

-		self._list_ctrl.AddDatas( url_matches )
+		self._list_ctrl.AddDatas( url_classes )

		 self._list_ctrl.Sort( 0 )

@@ -6697,39 +6697,39 @@ class EditURLMatchesPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def _Add( self ):

-		url_match = lib.Networking.Domain.URLMatch( 'new url class' )
+		url_class = lib.Networking.Domain.URLClass( 'new url class' )

		 with lib.GUI.TopLevelWindows.DialogEdit( self, 'edit url class' ) as dlg:

-			panel = EditURLMatchPanel( dlg, url_match )
+			panel = EditURLClassPanel( dlg, url_class )

			 dlg.SetPanel( panel )

			 if dlg.ShowModal() == wx.ID_OK:

-				url_match = panel.GetValue()
+				url_class = panel.GetValue()

-				self._AddURLMatch( url_match )
+				self._AddURLClass( url_class )

				 self._list_ctrl.Sort()

-	def _AddURLMatch( self, url_match ):
+	def _AddURLClass( self, url_class ):

-		lib.Hydrus.Serialisable.SetNonDupeName( url_match, self._GetExistingNames() )
+		lib.Hydrus.Serialisable.SetNonDupeName( url_class, self._GetExistingNames() )

-		url_match.RegenerateMatchKey()
+		url_class.RegenerateMatchKey()

-		self._list_ctrl.AddDatas( ( url_match, ) )
+		self._list_ctrl.AddDatas( ( url_class, ) )

-	def _ConvertDataToListCtrlTuples( self, url_match ):
+	def _ConvertDataToListCtrlTuples( self, url_class ):

-		name = url_match.GetName()
-		url_type = url_match.GetURLType()
-		example_url = url_match.Normalise( url_match.GetExampleURL() )
+		name = url_class.GetName()
+		url_type = url_class.GetURLType()
+		example_url = url_class.Normalise( url_class.GetExampleURL() )

		 pretty_name = name
		 pretty_url_type = HC.url_type_string_lookup[ url_type ]
@@ -6743,23 +6743,23 @@ class EditURLMatchesPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def _Edit( self ):

-		for url_match in self._list_ctrl.GetData( only_selected = True ):
+		for url_class in self._list_ctrl.GetData( only_selected = True ):

			 with lib.GUI.TopLevelWindows.DialogEdit( self, 'edit url class' ) as dlg:

-				panel = EditURLMatchPanel( dlg, url_match )
+				panel = EditURLClassPanel( dlg, url_class )

				 dlg.SetPanel( panel )

				 if dlg.ShowModal() == wx.ID_OK:

-					self._list_ctrl.DeleteDatas( ( url_match, ) )
+					self._list_ctrl.DeleteDatas( ( url_class, ) )

-					url_match = panel.GetValue()
+					url_class = panel.GetValue()

-					lib.Hydrus.Serialisable.SetNonDupeName( url_match, self._GetExistingNames() )
+					lib.Hydrus.Serialisable.SetNonDupeName( url_class, self._GetExistingNames() )

-					self._list_ctrl.AddDatas( ( url_match, ) )
+					self._list_ctrl.AddDatas( ( url_class, ) )

				 else:

@@ -6773,9 +6773,9 @@ class EditURLMatchesPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def _GetExistingNames( self ):

-		url_matches = self._list_ctrl.GetData()
+		url_classes = self._list_ctrl.GetData()

-		names = { url_match.GetName() for url_match in url_matches }
+		names = { url_class.GetName() for url_class in url_classes }

		 return names

@@ -6790,28 +6790,28 @@ class EditURLMatchesPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 else:

-			url_matches = self.GetValue()
+			url_classes = self.GetValue()

			 domain_manager = lib.Networking.Domain.NetworkDomainManager()

			 domain_manager.Initialise()

-			domain_manager.SetURLMatches( url_matches )
+			domain_manager.SetURLClasses( url_classes )

			 try:

-				url_match = domain_manager.GetURLMatch( url )
+				url_class = domain_manager.GetURLClass( url )

-				if url_match is None:
+				if url_class is None:

					 text = 'No match!'

				 else:

-					text = 'Matches "' + url_match.GetName() + '"'
+					text = 'Matches "' + url_class.GetName() + '"'

-			except lib.Hydrus.Exceptions.URLMatchException as e:
+			except lib.Hydrus.Exceptions.URLClassException as e:

				 text = str( e )

@@ -6827,19 +6827,19 @@ class EditURLMatchesPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def GetValue( self ):

-		url_matches = self._list_ctrl.GetData()
+		url_classes = self._list_ctrl.GetData()

-		return url_matches
+		return url_classes

-class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):
+class EditURLClassLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

-	def __init__( self, parent, network_engine, url_matches, parsers, url_match_keys_to_parser_keys ):
+	def __init__( self, parent, network_engine, url_classes, parsers, url_class_keys_to_parser_keys ):

		 lib.GUI.ScrolledPanels.EditPanel.__init__( self, parent )

-		self._url_matches = url_matches
-		self._url_match_keys_to_url_matches = { url_match.GetMatchKey() : url_match for url_match in self._url_matches }
+		self._url_classes = url_classes
+		self._url_class_keys_to_url_classes = { url_class.GetMatchKey() : url_class for url_class in self._url_classes }

		 self._parsers = parsers
		 self._parser_keys_to_parsers = { parser.GetParserKey() : parser for parser in self._parsers }
@@ -6854,7 +6854,7 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 columns = [ ( 'url class', -1 ), ( 'api url class', 36 ) ]

-		self._api_pairs_list_ctrl = lib.GUI.ListCtrl.BetterListCtrl( self._notebook, 'url_match_api_pairs', 10, 36, columns, self._ConvertAPIPairDataToListCtrlTuples )
+		self._api_pairs_list_ctrl = lib.GUI.ListCtrl.BetterListCtrl( self._notebook, 'url_class_api_pairs', 10, 36, columns, self._ConvertAPIPairDataToListCtrlTuples )

		 #

@@ -6862,53 +6862,53 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 columns = [ ( 'url class', -1 ), ( 'url type', 20 ), ( 'parser', 36 ) ]

-		self._parser_list_ctrl = lib.GUI.ListCtrl.BetterListCtrl( self._parser_list_ctrl_panel, 'url_match_keys_to_parser_keys', 24, 36, columns, self._ConvertParserDataToListCtrlTuples, activation_callback = self._EditParser )
+		self._parser_list_ctrl = lib.GUI.ListCtrl.BetterListCtrl( self._parser_list_ctrl_panel, 'url_class_keys_to_parser_keys', 24, 36, columns, self._ConvertParserDataToListCtrlTuples, activation_callback = self._EditParser )

		 self._parser_list_ctrl_panel.SetListCtrl( self._parser_list_ctrl )

		 self._parser_list_ctrl_panel.AddButton( 'edit', self._EditParser, enabled_only_on_selection = True )
		 self._parser_list_ctrl_panel.AddButton( 'clear', self._ClearParser, enabled_check_func = self._LinksOnCurrentSelection )
-		self._parser_list_ctrl_panel.AddButton( 'try to fill in gaps based on example urls', self._TryToLinkUrlMatchesAndParsers, enabled_check_func = self._GapsExist )
+		self._parser_list_ctrl_panel.AddButton( 'try to fill in gaps based on example urls', self._TryToLinkURLClassesAndParsers, enabled_check_func = self._GapsExist )

		 #

-		api_pairs = lib.Networking.Domain.ConvertURLMatchesIntoAPIPairs( url_matches )
+		api_pairs = lib.Networking.Domain.ConvertURLClassesIntoAPIPairs( url_classes )

		 self._api_pairs_list_ctrl.AddDatas( api_pairs )

		 self._api_pairs_list_ctrl.Sort( 0 )

		 # anything that goes to an api url will be parsed by that api's parser--it can't have its own
-		api_pair_unparsable_url_matches = set()
+		api_pair_unparsable_url_classes = set()

		 for ( a, b ) in api_pairs:

-			api_pair_unparsable_url_matches.add( a )
+			api_pair_unparsable_url_classes.add( a )

		 #

		 listctrl_data = []

-		for url_match in url_matches:
+		for url_class in url_classes:

-			if not url_match.IsParsable() or url_match in api_pair_unparsable_url_matches:
+			if not url_class.IsParsable() or url_class in api_pair_unparsable_url_classes:

				 continue

-			url_match_key = url_match.GetMatchKey()
+			url_class_key = url_class.GetMatchKey()

-			if url_match_key in url_match_keys_to_parser_keys:
+			if url_class_key in url_class_keys_to_parser_keys:

-				parser_key = url_match_keys_to_parser_keys[ url_match_key ]
+				parser_key = url_class_keys_to_parser_keys[ url_class_key ]

			 else:

				 parser_key = None

-			listctrl_data.append( ( url_match_key, parser_key ) )
+			listctrl_data.append( ( url_class_key, parser_key ) )

		 self._parser_list_ctrl.AddDatas( listctrl_data )
@@ -6939,9 +6939,9 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

					 self._parser_list_ctrl.DeleteDatas( ( data, ) )

-					( url_match_key, parser_key ) = data
+					( url_class_key, parser_key ) = data

-					new_data = ( url_match_key, None )
+					new_data = ( url_class_key, None )

					 self._parser_list_ctrl.AddDatas( ( new_data, ) )

@@ -6969,13 +6969,13 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def _ConvertParserDataToListCtrlTuples( self, data ):

-		( url_match_key, parser_key ) = data
+		( url_class_key, parser_key ) = data

-		url_match = self._url_match_keys_to_url_matches[ url_match_key ]
+		url_class = self._url_class_keys_to_url_classes[ url_class_key ]

-		url_match_name = url_match.GetName()
+		url_class_name = url_class.GetName()

-		url_type = url_match.GetURLType()
+		url_type = url_class.GetURLType()

		 if parser_key is None:

@@ -6988,14 +6988,14 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):
			 parser_name = parser.GetName()

-		pretty_url_match_name = url_match_name
+		pretty_url_class_name = url_class_name

		 pretty_url_type = HC.url_type_string_lookup[ url_type ]

		 pretty_parser_name = parser_name

-		display_tuple = ( pretty_url_match_name, pretty_url_type, pretty_parser_name )
-		sort_tuple = ( url_match_name, pretty_url_type, parser_name )
+		display_tuple = ( pretty_url_class_name, pretty_url_type, pretty_parser_name )
+		sort_tuple = ( url_class_name, pretty_url_type, parser_name )

		 return ( display_tuple, sort_tuple )

@@ -7011,15 +7011,15 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 for data in self._parser_list_ctrl.GetData( only_selected = True ):

-			( url_match_key, parser_key ) = data
+			( url_class_key, parser_key ) = data

-			url_match = self._url_match_keys_to_url_matches[ url_match_key ]
+			url_class = self._url_class_keys_to_url_classes[ url_class_key ]

			 choice_tuples = [ ( parser.GetName(), parser ) for parser in self._parsers ]

			 try:

-				parser = lib.GUI.Dialogs.Quick.SelectFromList( self, 'select parser for ' + url_match.GetName(), choice_tuples )
+				parser = lib.GUI.Dialogs.Quick.SelectFromList( self, 'select parser for ' + url_class.GetName(), choice_tuples )

			 except lib.Hydrus.Exceptions.CancelledException:

@@ -7028,7 +7028,7 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

			 self._parser_list_ctrl.DeleteDatas( ( data, ) )

-			new_data = ( url_match_key, parser.GetParserKey() )
+			new_data = ( url_class_key, parser.GetParserKey() )

			 self._parser_list_ctrl.AddDatas( ( new_data, ) )

@@ -7038,31 +7038,31 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def _GapsExist( self ):

-		return None in ( parser_key for ( url_match_key, parser_key ) in self._parser_list_ctrl.GetData() )
+		return None in ( parser_key for ( url_class_key, parser_key ) in self._parser_list_ctrl.GetData() )

	 def _LinksOnCurrentSelection( self ):

-		non_none_parser_keys = [ parser_key for ( url_match_key, parser_key ) in self._parser_list_ctrl.GetData( only_selected = True ) if parser_key is not None ]
+		non_none_parser_keys = [ parser_key for ( url_class_key, parser_key ) in self._parser_list_ctrl.GetData( only_selected = True ) if parser_key is not None ]

		 return len( non_none_parser_keys ) > 0

-	def _TryToLinkUrlMatchesAndParsers( self ):
+	def _TryToLinkURLClassesAndParsers( self ):

-		existing_url_match_keys_to_parser_keys = { url_match_key : parser_key for ( url_match_key, parser_key ) in self._parser_list_ctrl.GetData() if parser_key is not None }
+		existing_url_class_keys_to_parser_keys = { url_class_key : parser_key for ( url_class_key, parser_key ) in self._parser_list_ctrl.GetData() if parser_key is not None }

-		new_url_match_keys_to_parser_keys = lib.Networking.Domain.NetworkDomainManager.STATICLinkURLMatchesAndParsers( self._url_matches, self._parsers, existing_url_match_keys_to_parser_keys )
+		new_url_class_keys_to_parser_keys = lib.Networking.Domain.NetworkDomainManager.STATICLinkURLClassesAndParsers( self._url_classes, self._parsers, existing_url_class_keys_to_parser_keys )

-		if len( new_url_match_keys_to_parser_keys ) > 0:
+		if len( new_url_class_keys_to_parser_keys ) > 0:

			 removees = []

			 for row in self._parser_list_ctrl.GetData():

-				( url_match_key, parser_key ) = row
+				( url_class_key, parser_key ) = row

-				if url_match_key in new_url_match_keys_to_parser_keys:
+				if url_class_key in new_url_class_keys_to_parser_keys:

					 removees.append( row )

@@ -7070,7 +7070,7 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

			 self._parser_list_ctrl.DeleteDatas( removees )

-			self._parser_list_ctrl.AddDatas( list(new_url_match_keys_to_parser_keys.items()) )
+			self._parser_list_ctrl.AddDatas( list(new_url_class_keys_to_parser_keys.items()) )

			 self._parser_list_ctrl.Sort()

@@ -7078,8 +7078,8 @@ class EditURLMatchLinksPanel( lib.GUI.ScrolledPanels.EditPanel ):

	 def GetValue( self ):

-		url_match_keys_to_parser_keys = { url_match_key : parser_key for ( url_match_key, parser_key ) in self._parser_list_ctrl.GetData() if parser_key is not None }
+		url_class_keys_to_parser_keys = { url_class_key : parser_key for ( url_class_key, parser_key ) in self._parser_list_ctrl.GetData() if parser_key is not None }

-		return url_match_keys_to_parser_keys
+		return url_class_keys_to_parser_keys

diff --git a/include/lib.GUI.ScrolledPanels.Management.py b/include/lib.GUI.ScrolledPanels.Management.py
index a5cca5e..c8955ab 100644
--- a/include/lib.GUI.ScrolledPanels.Management.py
+++ b/include/lib.GUI.ScrolledPanels.Management.py
@@ -3500,9 +3500,9 @@ class ManageOptionsPanel( lib.GUI.ScrolledPanels.ManagePanel ):
			 message += os.linesep * 2
			 message += 'To get around this, the client populates a pre-boot and ongoing disk cache. By contiguously frontloading the database into memory, the most important functions do not need to wait on your disk for most of their work.'
			 message += os.linesep * 2
-			message += 'If you tend to leave your client on in the background and have a slow drive but a lot of ram, you might like to pump these numbers up. 15s boot cache and 2048MB ongoing can really make a difference on, for instance, a slow laptop drive.'
+			message += 'If you tend to leave your client on in the background and have a slow drive but a lot of ram, you might like to pump these numbers up. 10s boot cache and 1024MB ongoing can really make a difference on, for instance, a slow laptop drive.'
			 message += os.linesep * 2
-			message += 'If you run the database from an SSD, you can reduce or entirely eliminate these values, as the benefit is not so stark. 2s and 256MB is fine.'
+			message += 'If you run the database from an SSD, you can reduce or entirely eliminate these values, as the benefit is not so stark. 2s and 256MB is plenty.'
			 message += os.linesep * 2
			 message += 'Unless you are testing, do not go crazy with this stuff. You can set 8192MB if you like, but there are diminishing (and potentially negative) returns.'

diff --git a/include/lib.GUI.ScrolledPanels.Review.py b/include/lib.GUI.ScrolledPanels.Review.py
index 4140b36..054804e 100644
--- a/include/lib.GUI.ScrolledPanels.Review.py
+++ b/include/lib.GUI.ScrolledPanels.Review.py
@@ -1483,7 +1483,7 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):
	 def _ImportPaths( self, paths ):

		 gugs = []
-		url_matches = []
+		url_classes = []
		 parsers = []
		 domain_metadatas = []
		 login_scripts = []
@@ -1518,7 +1518,7 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):
				 continue

-			if isinstance( obj_list, ( lib.Networking.Domain.GalleryURLGenerator, lib.Networking.Domain.NestedGalleryURLGenerator, lib.Networking.Domain.URLMatch, lib.Handling.Parsing.PageParser, lib.Networking.Domain.DomainMetadataPackage, lib.Networking.Login.LoginScriptDomain ) ):
+			if isinstance( obj_list, ( lib.Networking.Domain.GalleryURLGenerator, lib.Networking.Domain.NestedGalleryURLGenerator, lib.Networking.Domain.URLClass, lib.Handling.Parsing.PageParser, lib.Networking.Domain.DomainMetadataPackage, lib.Networking.Login.LoginScriptDomain ) ):

				 obj_list = lib.Hydrus.Serialisable.SerialisableList( [ obj_list ] )

@@ -1536,9 +1536,9 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):

					 gugs.append( obj )

-				elif isinstance( obj, lib.Networking.Domain.URLMatch ):
+				elif isinstance( obj, lib.Networking.Domain.URLClass ):

-					url_matches.append( obj )
+					url_classes.append( obj )

				 elif isinstance( obj, lib.Handling.Parsing.PageParser ):

@@ -1571,30 +1571,46 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):

		 # url matches first

-		dupe_url_matches = []
-		num_exact_dupe_url_matches = 0
-		new_url_matches = []
+		url_class_names_seen = set()

-		for url_match in url_matches:
+		dupe_url_classes = []
+		num_exact_dupe_url_classes = 0
+		new_url_classes = []
+
+		for url_class in url_classes:
+
+			if url_class.GetName() in url_class_names_seen:
+
+				continue
+

-			if domain_manager.AlreadyHaveExactlyThisURLMatch( url_match ):
+			if domain_manager.AlreadyHaveExactlyThisURLClass( url_class ):

-				dupe_url_matches.append( url_match )
-				num_exact_dupe_url_matches += 1
+				dupe_url_classes.append( url_class )
+				num_exact_dupe_url_classes += 1

			 else:

-				new_url_matches.append( url_match )
+				new_url_classes.append( url_class )
+
+				url_class_names_seen.add( url_class.GetName() )

		 # now gugs

+		gug_names_seen = set()
+
		 num_exact_dupe_gugs = 0
		 new_gugs = []

		 for gug in gugs:

+			if gug.GetName() in gug_names_seen:
+
+				continue
+
+
			 if domain_manager.AlreadyHaveExactlyThisGUG( gug ):

				 num_exact_dupe_gugs += 1
@@ -1603,15 +1619,24 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):

				 new_gugs.append( gug )

+				gug_names_seen.add( gug.GetName() )
+

		 # now parsers

+		parser_names_seen = set()
+
		 num_exact_dupe_parsers = 0
		 new_parsers = []

		 for parser in parsers:

+			if parser.GetName() in parser_names_seen:
+
+				continue
+
+
			 if domain_manager.AlreadyHaveExactlyThisParser( parser ):

				 num_exact_dupe_parsers += 1
@@ -1620,15 +1645,24 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):

				 new_parsers.append( parser )

+				parser_names_seen.add( parser.GetName() )
+

		 # now login scripts

+		login_script_names_seen = set()
+
		 num_exact_dupe_login_scripts = 0
		 new_login_scripts = []

		 for login_script in login_scripts:

+			if login_script.GetName() in login_script_names_seen:
+
+				continue
+
+
			 if login_manager.AlreadyHaveExactlyThisLoginScript( login_script ):

				 num_exact_dupe_login_scripts += 1
@@ -1637,10 +1671,14 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):

				 new_login_scripts.append( login_script )

+				login_script_names_seen.add( login_script.GetName() )
+

		 # now domain metadata

+		domains_seen = set()
+
		 num_exact_dupe_domain_metadatas = 0
		 new_domain_metadatas = []

@@ -1652,6 +1690,11 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):
			 headers_list = None
			 bandwidth_rules = None

+			if domain in domains_seen:
+
+				continue
+
+
			 nc = lib.Networking.Contexts.NetworkContext( CC.NETWORK_CONTEXT_DOMAIN, domain )

			 if domain_metadata.HasHeaders():
@@ -1684,13 +1727,15 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):

				 new_domain_metadatas.append( new_dm )

+				domains_seen.add( domain )
+

		 #

-		total_num_dupes = num_exact_dupe_gugs + num_exact_dupe_url_matches + num_exact_dupe_parsers + num_exact_dupe_domain_metadatas + num_exact_dupe_login_scripts
+		total_num_dupes = num_exact_dupe_gugs + num_exact_dupe_url_classes + num_exact_dupe_parsers + num_exact_dupe_domain_metadatas + num_exact_dupe_login_scripts

-		if len( new_gugs ) + len( new_url_matches ) + len( new_parsers ) + len( new_domain_metadatas ) + len( new_login_scripts ) == 0:
+		if len( new_gugs ) + len( new_url_classes ) + len( new_parsers ) + len( new_domain_metadatas ) + len( new_login_scripts ) == 0:

			 wx.MessageBox( 'All ' + lib.Hydrus.Data.ToHumanInt( total_num_dupes ) + ' downloader objects in that package appeared to already be in the client, so nothing need be added.' )

@@ -1703,7 +1748,7 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):

			 choice_tuples = []
			 choice_tuples.extend( [ ( 'GUG: ' + gug.GetName(), gug, True ) for gug in new_gugs ] )
-			choice_tuples.extend( [ ( 'URL Class: ' + url_match.GetName(), url_match, True ) for url_match in new_url_matches ] )
+			choice_tuples.extend( [ ( 'URL Class: ' + url_class.GetName(), url_class, True ) for url_class in new_url_classes ] )
			 choice_tuples.extend( [ ( 'Parser: ' + parser.GetName(), parser, True ) for parser in new_parsers ] )
			 choice_tuples.extend( [ ( 'Login Script: ' + login_script.GetName(), login_script, True ) for login_script in new_login_scripts ] )
			 choice_tuples.extend( [ ( 'Domain Metadata: ' + domain_metadata.GetDomain(), domain_metadata, True ) for domain_metadata in new_domain_metadatas ] )
@@ -1719,7 +1764,7 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):
					 new_objects = panel.GetValue()

					 new_gugs = [ obj for obj in new_objects if isinstance( obj, ( lib.Networking.Domain.GalleryURLGenerator, lib.Networking.Domain.NestedGalleryURLGenerator ) ) ]
-					new_url_matches = [ obj for obj in new_objects if isinstance( obj, lib.Networking.Domain.URLMatch ) ]
+					new_url_classes = [ obj for obj in new_objects if isinstance( obj, lib.Networking.Domain.URLClass ) ]
					 new_parsers = [ obj for obj in new_objects if isinstance( obj, lib.Handling.Parsing.PageParser ) ]
					 new_login_scripts = [ obj for obj in new_objects if isinstance( obj, lib.Networking.Login.LoginScriptDomain ) ]
					 new_domain_metadatas = [ obj for obj in new_objects if isinstance( obj, lib.Networking.Domain.DomainMetadataPackage ) ]
@@ -1734,7 +1779,7 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):
		 # final ask

		 new_gugs.sort( key = lambda o: o.GetName() )
-		new_url_matches.sort( key = lambda o: o.GetName() )
+		new_url_classes.sort( key = lambda o: o.GetName() )
		 new_parsers.sort( key = lambda o: o.GetName() )
		 new_login_scripts.sort( key = lambda o: o.GetName() )
		 new_domain_metadatas.sort( key = lambda o: o.GetDomain() )
@@ -1760,7 +1805,7 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):

		 all_to_add = list( new_gugs )
-		all_to_add.extend( new_url_matches )
+		all_to_add.extend( new_url_classes )
		 all_to_add.extend( new_parsers )
		 all_to_add.extend( new_login_scripts )
		 all_to_add.extend( new_domain_metadatas )
@@ -1793,14 +1838,14 @@ class ReviewDownloaderImport( lib.GUI.ScrolledPanels.ReviewPanel ):
			 domain_manager.AddGUGs( new_gugs )

-		domain_manager.AutoAddURLMatchesAndParsers( new_url_matches, dupe_url_matches, new_parsers )
+		domain_manager.AutoAddURLClassesAndParsers( new_url_classes, dupe_url_classes, new_parsers )

		 bandwidth_manager.AutoAddDomainMetadatas( new_domain_metadatas )
		 domain_manager.AutoAddDomainMetadatas( new_domain_metadatas, approved = True )
		 login_manager.AutoAddLoginScripts( new_login_scripts )

		 num_new_gugs = len( new_gugs )
-		num_aux = len( new_url_matches ) + len( new_parsers ) + len( new_login_scripts ) + len( new_domain_metadatas )
+		num_aux = len( new_url_classes ) + len( new_parsers ) + len( new_login_scripts ) + len( new_domain_metadatas )

		 final_message = 'Successfully added ' + lib.Hydrus.Data.ToHumanInt( num_new_gugs ) + ' new downloaders and ' + lib.Hydrus.Data.ToHumanInt( num_aux ) + ' auxiliary objects.'

@@ -2703,13 +2748,13 @@ class ReviewServicesPanel( lib.GUI.ScrolledPanels.ReviewPanel ):

		 lib.GUI.ScrolledPanels.ReviewPanel.__init__( self, parent )

-		self._notebook = wx.Notebook( self )
+		self._notebook = lib.GUI.Common.BetterNotebook( self )

-		self._local_listbook = lib.GUI.Common.ListBook( self._notebook )
-		self._remote_listbook = lib.GUI.Common.ListBook( self._notebook )
+		self._local_notebook = lib.GUI.Common.BetterNotebook( self._notebook )
+		self._remote_notebook = lib.GUI.Common.BetterNotebook( self._notebook )

-		self._notebook.AddPage( self._local_listbook, 'local' )
-		self._notebook.AddPage( self._remote_listbook, 'remote' )
+		self._notebook.AddPage( self._local_notebook, 'local' )
+		self._notebook.AddPage( self._remote_notebook, 'remote' )

		 self._InitialiseServices()

@@ -2739,24 +2784,22 @@ class ReviewServicesPanel( lib.GUI.ScrolledPanels.ReviewPanel ):
			 previous_service_key = page.GetServiceKey()

-		self._local_listbook.DeleteAllPages()
-		self._remote_listbook.DeleteAllPages()
+		self._local_notebook.DeleteAllPages()
+		self._remote_notebook.DeleteAllPages()

-		listbook_dict = {}
+		notebook_dict = {}

		 services = self._controller.services_manager.GetServices( randomised = False )

-		lb_to_select = None
-		service_type_name_to_select = None
+		local_remote_notebook_to_select = None
		 service_type_lb = None
-		service_name_to_select = None

		 for service in services:

			 service_type = service.GetServiceType()

-			if service_type in HC.LOCAL_SERVICES:
				parent_listbook = self._local_listbook
-			else:
				parent_listbook = self._remote_listbook
+			if service_type in HC.LOCAL_SERVICES:
				parent_notebook = self._local_notebook
+			else:
				parent_notebook = self._remote_notebook

			 if service_type == HC.TAG_REPOSITORY:

			 	service_type_name = 'tag repositories'
			 elif service_type == HC.FILE_REPOSITORY:

			 	service_type_name = 'file repositories'
@@ -2771,51 +2814,34 @@ class ReviewServicesPanel( lib.GUI.ScrolledPanels.ReviewPanel ):
			 elif service_type == HC.IPFS:

			 	service_type_name = 'ipfs'
			 else:

			 	continue

-			if service_type_name not in listbook_dict:
+			if service_type_name not in notebook_dict:

-				listbook = lib.GUI.Common.ListBook( parent_listbook )
+				services_notebook = lib.GUI.Common.BetterNotebook( parent_notebook )

-				listbook_dict[ service_type_name ] = listbook
+				notebook_dict[ service_type_name ] = services_notebook

-				parent_listbook.AddPage( service_type_name, service_type_name, listbook )
+				parent_notebook.AddPage( services_notebook, service_type_name, select = False )

-			listbook = listbook_dict[ service_type_name ]
-
-			name = service.GetName()
-
-			panel_class = ClientGUIPanels.ReviewServicePanel
+			services_notebook = notebook_dict[ service_type_name ]

-			listbook.AddPageArgs( name, name, panel_class, ( listbook, service ), {} )
+			page = ClientGUIPanels.ReviewServicePanel( services_notebook, service )

			 if service.GetServiceKey() == previous_service_key:

-				lb_to_select = parent_listbook
-				service_type_name_to_select = service_name_to_select
-				service_type_lb = listbook
-				name_to_select = name
+				self._notebook.SelectPage( parent_notebook )
+				parent_notebook.SelectPage( services_notebook )

-
-
-		if lb_to_select is not None:
-
-			if self._notebook.GetCurrentPage() != lb_to_select:
+				select = True

-				selection = self._notebook.GetSelection()
+			else:

-				if selection == 0:
-
-					self._notebook.SetSelection( 1 )
-
-				else:
-
-					self._notebook.SetSelection( 0 )
-
+				select = False

-			lb_to_select.Select( service_name_to_select )
+			name = service.GetName()

-			service_type_lb.Select( name_to_select )
+			services_notebook.AddPage( page, name, select = select )

diff --git a/include/lib.GUI.Tags.py b/include/lib.GUI.Tags.py
index da84db4..724695c 100644
--- a/include/lib.GUI.Tags.py
+++ b/include/lib.GUI.Tags.py
@@ -46,7 +46,7 @@ class EditTagFilterPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 #

-		self._notebook = wx.Notebook( self )
+		self._notebook = lib.GUI.Common.BetterNotebook( self )

		 #

@@ -82,22 +82,22 @@ class EditTagFilterPanel( lib.GUI.ScrolledPanels.EditPanel ):

		 if prefer_blacklist:

-			selection_tests.append( ( blacklist_possible, 0 ) )
-			selection_tests.append( ( whitelist_possible, 1 ) )
-			selection_tests.append( ( True, 2 ) )
+			selection_tests.append( ( blacklist_possible, self._blacklist_panel ) )
+			selection_tests.append( ( whitelist_possible, self._whitelist_panel ) )
+			selection_tests.append( ( True, self._advanced_panel ) )

		 else:

-			selection_tests.append( ( whitelist_possible, 0 ) )
-			selection_tests.append( ( blacklist_possible, 1 ) )
-			selection_tests.append( ( True, 2 ) )
+			selection_tests.append( ( whitelist_possible, self._whitelist_panel ) )
+			selection_tests.append( ( blacklist_possible, self._blacklist_panel ) )
+			selection_tests.append( ( True, self._advanced_panel ) )

-		for ( test, index ) in selection_tests:
+		for ( test, page ) in selection_tests:

			 if test:

-				self._notebook.SetSelection( index )
+				self._notebook.SelectPage( page )

				 break

diff --git a/include/lib.GUI.TopLevelWindows.py b/include/lib.GUI.TopLevelWindows.py
index af2352c..6aca575 100644
--- a/include/lib.GUI.TopLevelWindows.py
+++ b/include/lib.GUI.TopLevelWindows.py
@@ -302,8 +302,6 @@ class NewDialog( wx.Dialog ):

		 self.SetIcon( HG.client_controller.frame_icon )

-		self.Bind( wx.EVT_BUTTON, self.EventDialogButton )
-
		 self.Bind( wx.EVT_MENU_CLOSE, self.EventMenuClose )
		 self.Bind( wx.EVT_MENU_HIGHLIGHT_ALL, self.EventMenuHighlight )
		 self.Bind( wx.EVT_MENU_OPEN, self.EventMenuOpen )
@@ -689,10 +687,11 @@ class DialogApplyCancel( DialogThatTakesScrollablePanel ):

		 self._apply = wx.Button( self, id = wx.ID_OK, label = 'apply' )
		 self._apply.SetForegroundColour( ( 0, 128, 0 ) )
-		#self._apply.Bind( wx.EVT_BUTTON, self.EventOK )
+		self._apply.Bind( wx.EVT_BUTTON, self.EventDialogButton )

		 self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'cancel' )
		 self._cancel.SetForegroundColour( ( 128, 0, 0 ) )
+		self._cancel.Bind( wx.EVT_BUTTON, self.EventDialogButton )

		 if self._hide_buttons:

diff --git a/include/lib.Handling.Image.Local.py b/include/lib.Handling.Image.Local.py
index 7e214cd..8102e68 100644
--- a/include/lib.Handling.Image.Local.py
+++ b/include/lib.Handling.Image.Local.py
@@ -1,3 +1,4 @@
+import numpy
 import numpy.core.multiarray # important this comes before cv!
 import lib.Hydrus.Constants.Local as CC
 import cv2
@@ -8,25 +9,6 @@ import lib.Handling.Image
 import lib.Hydrus.Globals as HG
 from functools import reduce

-if cv2.__version__.startswith( '2' ):
-
-	CV_IMREAD_FLAGS_SUPPORTS_ALPHA = cv2.CV_LOAD_IMAGE_UNCHANGED
-	CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = CV_IMREAD_FLAGS_SUPPORTS_ALPHA
-
-	# there's something wrong with these, but I don't have an easy test env for it atm
-	# CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = cv2.CV_LOAD_IMAGE_ANYDEPTH | cv2.CV_LOAD_IMAGE_ANYCOLOR
-
-	CV_JPEG_THUMBNAIL_ENCODE_PARAMS = []
-	CV_PNG_THUMBNAIL_ENCODE_PARAMS = []
-
-else:
-
-	CV_IMREAD_FLAGS_SUPPORTS_ALPHA = cv2.IMREAD_UNCHANGED
-	CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = cv2.IMREAD_ANYDEPTH | cv2.IMREAD_ANYCOLOR # this preserves colour info but does EXIF reorientation and flipping
-
-	CV_JPEG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_JPEG_QUALITY, 92 ]
-	CV_PNG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_PNG_COMPRESSION, 9 ]
-
 cv_interpolation_enum_lookup = {}

 cv_interpolation_enum_lookup[ CC.ZOOM_NEAREST ] = cv2.INTER_NEAREST
@@ -35,113 +17,24 @@ cv_interpolation_enum_lookup[ CC.ZOOM_AREA ] = cv2.INTER_AREA
 cv_interpolation_enum_lookup[ CC.ZOOM_CUBIC ] = cv2.INTER_CUBIC
 cv_interpolation_enum_lookup[ CC.ZOOM_LANCZOS4 ] = cv2.INTER_LANCZOS4

-def GenerateNumpyImage( path, mime ):
-
-	if HG.media_load_report_mode:
-
-		lib.Hydrus.Data.ShowText( 'Loading media: ' + path )
-
-
-	if mime == HC.IMAGE_GIF or HG.client_controller.new_options.GetBoolean( 'load_images_with_pil' ):
-
-		if HG.media_load_report_mode:
-
-			lib.Hydrus.Data.ShowText( 'Loading with PIL' )
-
-
-		# a regular cv.imread call, can crash the whole process on random thumbs, hooray, so have this as backup
-		# it was just the read that was the problem, so this seems to work fine, even if pil is only about half as fast
-
-		pil_image = lib.Handling.Image.GeneratePILImage( path )
-
-		numpy_image = GenerateNumPyImageFromPILImage( pil_image )
-
-	else:
-
-		if HG.media_load_report_mode:
-
-			lib.Hydrus.Data.ShowText( 'Loading with OpenCV' )
-
-
-		if mime == HC.IMAGE_JPEG:
-
-			flags = CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION
-
-		else:
-
-			flags = CV_IMREAD_FLAGS_SUPPORTS_ALPHA
-
-
-		numpy_image = cv2.imread( path, flags = flags )
-
-		if numpy_image is None: # doesn't support static gifs and some random other stuff
-
-			if HG.media_load_report_mode:
-
-				lib.Hydrus.Data.ShowText( 'OpenCV Failed, loading with PIL' )
-
-
-			pil_image = lib.Handling.Image.GeneratePILImage( path )
-
-			numpy_image = GenerateNumPyImageFromPILImage( pil_image )
-
-		else:
-
-			if numpy_image.dtype == 'uint16':
-
-				numpy_image //= 256
-
-				numpy_image = numpy.array( numpy_image, dtype = 'uint8' )
-
-
-			shape = numpy_image.shape
-
-			if len( shape ) == 2:
-
-				# monochrome image
-
-				convert = cv2.COLOR_GRAY2RGB
-
-			else:
-
-				( im_y, im_x, depth ) = shape
-
-				if depth == 4:
-
-					convert = cv2.COLOR_BGRA2RGBA
-
-				else:
-
-					convert = cv2.COLOR_BGR2RGB
-
-
-
-			numpy_image = cv2.cvtColor( numpy_image, convert )
-
-
-
-	return numpy_image
+def GenerateNumPyImage( path, mime ):

-def GenerateNumPyImageFromPILImage( pil_image ):
+	force_pil = HG.client_controller.new_options.GetBoolean( 'load_images_with_pil' )

-	pil_image = lib.Handling.Image.Dequantize( pil_image )
-
-	( w, h ) = pil_image.size
-
-	s = pil_image.tobytes()
-
-	return numpy.fromstring( s, dtype = 'uint8' ).reshape( ( h, w, len( s ) // ( w * h ) ) )
+	return lib.Handling.Image.GenerateNumPyImage( path, mime, force_pil = force_pil )

 def GenerateShapePerceptualHashes( path, mime ):

-	numpy_image = GenerateNumpyImage( path, mime )
+	numpy_image = GenerateNumPyImage( path, mime )

	 ( y, x, depth ) = numpy_image.shape

	 if depth == 4:

		 # doing this on 10000x10000 pngs eats ram like mad
-		numpy_image = ThumbnailNumpyImage( numpy_image, ( 1024, 1024 ) )
+		target_resolution = lib.Handling.Image.GetThumbnailResolution( ( x, y ), ( 1024, 1024 ) )
+
+		numpy_image = lib.Handling.Image.ResizeNumPyImage( numpy_image, target_resolution )

		 ( y, x, depth ) = numpy_image.shape

@@ -238,106 +131,19 @@ def GenerateShapePerceptualHashes( path, mime ):

	 phashes.add( phash )

-	phashes.discard( CC.BLANK_PHASH )
+	phashes = DiscardBlankPerceptualHashes( phashes )

	 # we good

	 return phashes

-def GenerateBytesFromCV( numpy_image, mime ):
-
-	( im_y, im_x, depth ) = numpy_image.shape
-
-	if depth == 4:
-
-		convert = cv2.COLOR_RGBA2BGRA
-
-	else:
-
-		convert = cv2.COLOR_RGB2BGR
-
-
-	numpy_image = cv2.cvtColor( numpy_image, convert )
-
-	if mime == HC.IMAGE_JPEG:
-
-		ext = '.jpg'
-
-		params = CV_JPEG_THUMBNAIL_ENCODE_PARAMS
-
-	else:
-
-		ext = '.png'
-
-		params = CV_PNG_THUMBNAIL_ENCODE_PARAMS
-
-
-	( result_success, result_byte_array ) = cv2.imencode( ext, numpy_image, params )
-
-	if result_success:
-
-		thumbnail_bytes = result_byte_array.tostring()
-
-		return thumbnail_bytes
-
-	else:
-
-		raise lib.Hydrus.Exceptions.CantRenderWithCVException( 'Thumb failed to encode!' )
-
-
-def GenerateThumbnailBytesFromStaticImagePathCV( path, bounding_dimensions, mime ):
-
-	if mime == HC.IMAGE_GIF:
-
-		return lib.Handling.GenerateThumbnailBytesFromStaticImagePathPIL( path, bounding_dimensions, mime )
-
-
-	numpy_image = GenerateNumpyImage( path, mime )
-
-	thumbnail_numpy_image = ThumbnailNumpyImage( numpy_image, bounding_dimensions )
-
-	try:
-
-		thumbnail_bytes = GenerateBytesFromCV( thumbnail_numpy_image, mime )
-
-		return thumbnail_bytes
-
-	except lib.Hydrus.Exceptions.CantRenderWithCVException:
-
-		return lib.Handling.GenerateThumbnailBytesFromStaticImagePathPIL( path, bounding_dimensions, mime )
-
-
-import lib.Handling
-
-lib.Handling.GenerateThumbnailBytesFromStaticImagePath = GenerateThumbnailBytesFromStaticImagePathCV
-
-def GetNumPyImageResolution( numpy_image ):
-
-	( image_height, image_width, depth ) = numpy_image.shape
+def DiscardBlankPerceptualHashes( phashes ):

-	return ( image_width, image_height )
-
-def ResizeNumpyImage( numpy_image, target_resolution ):
-
-	( target_width, target_height ) = target_resolution
-	( image_width, image_height, depth ) = numpy_image.shape
-
-	if target_width == image_width and target_height == target_width:
-
-		return numpy_image
-
-	elif target_width > image_height or target_height > image_width:
-
-		interpolation = cv2.INTER_LANCZOS4
-
-	else:
-
-		interpolation = cv2.INTER_AREA
-
+	phashes = { phash for phash in phashes if lib.Hydrus.Data.Get64BitHammingDistance( phash, CC.BLANK_PHASH ) > 4 }

-	return cv2.resize( numpy_image, ( target_width, target_height ), interpolation = interpolation )
+	return phashes

-def ResizeNumpyImageForMediaViewer( mime, numpy_image, target_resolution ):
+def ResizeNumPyImageForMediaViewer( mime, numpy_image, target_resolution ):

	 ( target_width, target_height ) = target_resolution
	 new_options = HG.client_controller.new_options
@@ -364,17 +170,3 @@ def ResizeNumpyImageForMediaViewer( mime, numpy_image, target_resolution ):
		 return cv2.resize( numpy_image, ( target_width, target_height ), interpolation = interpolation )

-def ThumbnailNumpyImage( numpy_image, bounding_dimensions ):
-
-	( bounding_width, bounding_height ) = bounding_dimensions
-	( image_height, image_width, depth ) = numpy_image.shape
-
-	if bounding_width >= image_height and bounding_height >= image_width:
-
-		return numpy_image
-
-
-	target_resolution = lib.Handling.Image.GetThumbnailResolution( ( image_width, image_height ), ( bounding_width, bounding_height ) )
-
-	return ResizeNumpyImage( numpy_image, target_resolution )
-
diff --git a/include/lib.Actions.Import.FileSeeds.py b/include/lib.Actions.Import.FileSeeds.py
index 6c41b12..97d6eee 100644
--- a/include/lib.Actions.Import.FileSeeds.py
+++ b/include/lib.Actions.Import.FileSeeds.py
@@ -170,7 +170,7 @@ class FileImportJob( object ):
		 self._pre_import_status = None

		 self._file_info = None
-		self._thumbnail = None
+		self._thumbnail_bytes = None
		 self._phashes = None
		 self._extra_hashes = None

@@ -182,6 +182,85 @@ class FileImportJob( object ):
		 self._file_import_options.CheckFileIsValid( size, mime, width, height )

+	def DoWork( self ):
+
+		if HG.file_report_mode:
+
+			lib.Hydrus.Data.ShowText( 'New file import job!' )
+
+
+		( pre_import_status, hash, note ) = self.GenerateHashAndStatus()
+
+		if self.IsNewToDB():
+
+			self.GenerateInfo()
+
+			self.CheckIsGoodToImport()
+
+			mime = self.GetMime()
+
+			HG.client_controller.client_files_manager.AddFile( hash, mime, self._temp_path, thumbnail_bytes = self._thumbnail_bytes )
+
+			( import_status, note ) = HG.client_controller.WriteSynchronous( 'import_file', self )
+
+		else:
+
+			import_status = pre_import_status
+
+
+		self.PubsubContentUpdates()
+
+		return ( import_status, hash, note )
+
+
+	def GenerateHashAndStatus( self ):
+
+		lib.Handling.Image.ConvertToPngIfBmp( self._temp_path )
+
+		self._hash = lib.Handling.GetHashFromPath( self._temp_path )
+
+		( self._pre_import_status, hash, note ) = HG.client_controller.Read( 'hash_status', 'sha256', self._hash, prefix = 'file recognised' )
+
+		return ( self._pre_import_status, self._hash, note )
+
+
+	def GenerateInfo( self ):
+
+		mime = lib.Handling.GetMime( self._temp_path )
+
+		new_options = HG.client_controller.new_options
+
+		if mime in HC.DECOMPRESSION_BOMB_IMAGES and not self._file_import_options.AllowsDecompressionBombs():
+
+			if lib.Handling.Image.IsDecompressionBomb( self._temp_path ):
+
+				raise lib.Hydrus.Exceptions.DecompressionBombException( 'Image seems to be a Decompression Bomb!' )
+
+
+
+		self._file_info = lib.Handling.GetFileInfo( self._temp_path, mime )
+
+		( size, mime, width, height, duration, num_frames, num_words ) = self._file_info
+
+		if mime in HC.MIMES_WITH_THUMBNAILS:
+
+			bounding_dimensions = HG.client_controller.options[ 'thumbnail_dimensions' ]
+
+			target_resolution = lib.Handling.Image.GetThumbnailResolution( ( width, height ), bounding_dimensions )
+
+			percentage_in = HG.client_controller.new_options.GetInteger( 'video_thumbnail_percentage_in' )
+
+			self._thumbnail_bytes = lib.Handling.GenerateThumbnailBytes( self._temp_path, target_resolution, mime, duration, num_frames, percentage_in = percentage_in )
+
+
+		if mime in HC.MIMES_WE_CAN_PHASH:
+
+			self._phashes = lib.Handling.Image.Local.GenerateShapePerceptualHashes( self._temp_path, mime )
+
+
+		self._extra_hashes = lib.Handling.GetExtraHashesFromPath( self._temp_path )
+
+
	 def GetExtraHashes( self ):

		 return self._extra_hashes
@@ -219,11 +298,6 @@ class FileImportJob( object ):
		 return self._phashes

-	def GetTempPathAndThumbnail( self ):
-
-		return ( self._temp_path, self._thumbnail )
-
-
	 def PubsubContentUpdates( self ):

		 if self._pre_import_status == CC.STATUS_SUCCESSFUL_BUT_REDUNDANT:
@@ -255,52 +329,6 @@ class FileImportJob( object ):
		 return False

-	def GenerateHashAndStatus( self ):
-
-		lib.Handling.Image.ConvertToPngIfBmp( self._temp_path )
-
-		self._hash = lib.Handling.GetHashFromPath( self._temp_path )
-
-		( self._pre_import_status, hash, note ) = HG.client_controller.Read( 'hash_status', 'sha256', self._hash, prefix = 'file recognised' )
-
-		return ( self._pre_import_status, self._hash, note )
-
-
-	def GenerateInfo( self ):
-
-		mime = lib.Handling.GetMime( self._temp_path )
-
-		new_options = HG.client_controller.new_options
-
-		if mime in HC.DECOMPRESSION_BOMB_IMAGES and not self._file_import_options.AllowsDecompressionBombs():
-
-			if lib.Handling.Image.IsDecompressionBomb( self._temp_path ):
-
-				raise lib.Hydrus.Exceptions.DecompressionBombException( 'Image seems to be a Decompression Bomb!' )
-
-
-
-		self._file_info = lib.Handling.GetFileInfo( self._temp_path, mime )
-
-		( size, mime, width, height, duration, num_frames, num_words ) = self._file_info
-
-		if mime in HC.MIMES_WITH_THUMBNAILS:
-
-			bounding_dimensions = HG.client_controller.options[ 'thumbnail_dimensions' ]
-
-			percentage_in = HG.client_controller.new_options.GetInteger( 'video_thumbnail_percentage_in' )
-
-			self._thumbnail = lib.Handling.GenerateThumbnailBytes( self._temp_path, bounding_dimensions, mime, width, height, duration, num_frames, percentage_in = percentage_in )
-
-
-		if mime in HC.MIMES_WE_CAN_PHASH:
-
-			self._phashes = lib.Handling.Image.Local.GenerateShapePerceptualHashes( self._temp_path, mime )
-
-
-		self._extra_hashes = lib.Handling.GetExtraHashesFromPath( self._temp_path )
-
-
 FILE_SEED_TYPE_HDD = 0
 FILE_SEED_TYPE_URL = 1

@@ -394,7 +422,7 @@ class FileSeed( lib.Hydrus.Serialisable.SerialisableBase ):

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

-			except lib.Hydrus.Exceptions.URLMatchException:
+			except lib.Hydrus.Exceptions.URLClassException:

				 continue # not a url--something like "file:///C:/Users/Tall%20Man/Downloads/maxresdefault.jpg" ha ha ha

@@ -714,7 +742,7 @@ class FileSeed( lib.Hydrus.Serialisable.SerialisableBase ):

							 if url != my_url:

-								my_url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( my_url )
+								my_url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( my_url )

								 ( media_result, ) = HG.client_controller.Read( 'media_results', ( hash, ) )

@@ -724,9 +752,9 @@ class FileSeed( lib.Hydrus.Serialisable.SerialisableBase ):

									 if this_files_url != my_url:

-										this_url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( this_files_url )
+										this_url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( this_files_url )

-										if my_url_match == this_url_match:
+										if my_url_class == this_url_class:

											 # oh no, the file this source url refers to has a different known url in this same domain
											 # it is more likely that an edit on this site points to the original elsewhere
@@ -787,7 +815,7 @@ class FileSeed( lib.Hydrus.Serialisable.SerialisableBase ):

		 file_import_job = FileImportJob( temp_path, file_import_options )

-		( status, hash, note ) = HG.client_controller.client_files_manager.ImportFile( file_import_job )
+		( status, hash, note ) = file_import_job.DoWork()

		 self.SetStatus( status, note = note )
		 self.SetHash( hash )
diff --git a/include/lib.Actions.Import.GallerySeeds.py b/include/lib.Actions.Import.GallerySeeds.py
index a9489c4..85a8101 100644
--- a/include/lib.Actions.Import.GallerySeeds.py
+++ b/include/lib.Actions.Import.GallerySeeds.py
@@ -355,13 +355,13 @@ class GallerySeed( lib.Hydrus.Serialisable.SerialisableBase ):

					 # we have failed to parse a next page url, but we would still like one, so let's see if the url match can provide one

-					url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( url_to_check )
+					url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( url_to_check )

-					if url_match is not None and url_match.CanGenerateNextGalleryPage():
+					if url_class is not None and url_class.CanGenerateNextGalleryPage():

						 try:

-							next_page_url = url_match.GetNextGalleryPage( url_to_check )
+							next_page_url = url_class.GetNextGalleryPage( url_to_check )

							 next_page_urls = [ next_page_url ]

diff --git a/include/lib.Actions.Import.SimpleURLs.py b/include/lib.Actions.Import.SimpleURLs.py
index 40c8662..4514187 100644
--- a/include/lib.Actions.Import.SimpleURLs.py
+++ b/include/lib.Actions.Import.SimpleURLs.py
@@ -955,9 +955,9 @@ class URLsImport( lib.Hydrus.Serialisable.SerialisableBase ):

			 for url in urls:

-				url_match = HG.client_controller.network_engine.domain_manager.GetURLMatch( url )
+				url_class = HG.client_controller.network_engine.domain_manager.GetURLClass( url )

-				if url_match is None or url_match.GetURLType() in ( HC.URL_TYPE_FILE, HC.URL_TYPE_POST ):
+				if url_class is None or url_class.GetURLType() in ( HC.URL_TYPE_FILE, HC.URL_TYPE_POST ):

					 file_seed = lib.Actions.Import.FileSeeds.FileSeed( lib.Actions.Import.FileSeeds.FILE_SEED_TYPE_URL, url )

diff --git a/include/lib.Actions.Import.Subscriptions.py b/include/lib.Actions.Import.Subscriptions.py
index 407f440..48918b5 100644
--- a/include/lib.Actions.Import.Subscriptions.py
+++ b/include/lib.Actions.Import.Subscriptions.py
@@ -496,12 +496,12 @@ class Subscription( lib.Hydrus.Serialisable.SerialisableBaseNamed ):
			 presentation_hashes = []
			 presentation_hashes_fast = set()

+			starting_num_urls = file_seed_cache.GetFileSeedCount()
+			starting_num_unknown = file_seed_cache.GetFileSeedCount( CC.STATUS_UNKNOWN )
+			starting_num_done = starting_num_urls - starting_num_unknown
+
			 while True:

-				num_urls = file_seed_cache.GetFileSeedCount()
-				num_unknown = file_seed_cache.GetFileSeedCount( CC.STATUS_UNKNOWN )
-				num_done = num_urls - num_unknown
-
				 file_seed = file_seed_cache.GetNextFileSeed( CC.STATUS_UNKNOWN )

				 if file_seed is None:
@@ -541,9 +541,18 @@ class Subscription( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

				 try:

-					x_out_of_y = 'file ' + lib.Hydrus.Data.ConvertValueRangeToPrettyString( num_done + 1, num_urls ) + ': '
+					num_urls = file_seed_cache.GetFileSeedCount()
+					num_unknown = file_seed_cache.GetFileSeedCount( CC.STATUS_UNKNOWN )
+					num_done = num_urls - num_unknown
+
+					# 4001/4003 is not as useful as 1/3
+
+					human_num_urls = num_urls - starting_num_done
+					human_num_done = num_done - starting_num_done
+
+					x_out_of_y = 'file ' + lib.Hydrus.Data.ConvertValueRangeToPrettyString( human_num_done + 1, human_num_urls ) + ': '

-					job_key.SetVariable( 'popup_gauge_2', ( num_done, num_urls ) )
+					job_key.SetVariable( 'popup_gauge_2', ( human_num_done, human_num_urls ) )

					 def status_hook( text ):

diff --git a/include/lib.Hydrus.Server.LocalResources.py b/include/lib.Hydrus.Server.LocalResources.py
index d64d92c..1c51830 100644
--- a/include/lib.Hydrus.Server.LocalResources.py
+++ b/include/lib.Hydrus.Server.LocalResources.py
@@ -698,7 +698,7 @@ class HydrusResourceClientAPIRestrictedAddFile( HydrusResourceClientAPIRestricte

		 try:

-			( status, hash, note ) = HG.client_controller.client_files_manager.ImportFile( file_import_job )
+			( status, hash, note ) = file_import_job.DoWork()

		 except Exception:

diff --git a/include/lib.Hydrus.Media.py b/include/lib.Hydrus.Media.py
index 9161188..2b13b55 100644
--- a/include/lib.Hydrus.Media.py
+++ b/include/lib.Hydrus.Media.py
@@ -37,6 +37,14 @@ def FlattenMedia( media_list ):

	 return flat_media

+def GetDuplicateComparisonScore( shown_media, comparison_media ):
+
+	statements_and_scores = GetDuplicateComparisonStatements( shown_media, comparison_media )
+
+	total_score = sum( ( score for ( statement, score ) in statements_and_scores.values() ) )
+
+	return total_score
+
 def GetDuplicateComparisonStatements( shown_media, comparison_media ):

	 new_options = HG.client_controller.new_options
@@ -50,39 +58,46 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):

	 #

-	statements = []
-	score = 0
+	statements_and_scores = {}

	 # 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.' )
+	if s_size != c_size:

-		score -= duplicate_comparison_score_more_tags
+		size_ratio = s_size / c_size

-	elif size_ratio < 0.95:
+		if size_ratio > 2.0:
+
+			operator = '>>'
+			score = duplicate_comparison_score_much_higher_filesize
+
+		elif size_ratio > 1.05:
+
+			operator = '>'
+			score = duplicate_comparison_score_higher_filesize
+
+		elif size_ratio < 0.5:
+
+			operator = '<<'
+			score = -duplicate_comparison_score_much_higher_filesize
+
+		elif size_ratio < 0.95:
+
+			operator = '<'
+			score = -duplicate_comparison_score_higher_filesize
+
+		else:
+
+			operator = '\u2248'
+			score = 0
+

-		statements.append( 'This has a smaller filesize.' )
+		statement = '{} {} {}'.format( lib.Hydrus.Data.ToHumanBytes( s_size ), operator, lib.Hydrus.Data.ToHumanBytes( c_size ) )

-		score -= duplicate_comparison_score_higher_filesize
+		statements_and_scores[ 'filesize' ]  = ( statement, score )

	 # higher/same res
@@ -90,7 +105,7 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
	 s_resolution = shown_media.GetResolution()
	 c_resolution = comparison_media.GetResolution()

-	if s_resolution is not None and c_resolution is not None:
+	if s_resolution is not None and c_resolution is not None and s_resolution != c_resolution:

		 ( s_w, s_h ) = shown_media.GetResolution()
		 ( c_w, c_h ) = comparison_media.GetResolution()
@@ -99,36 +114,34 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):

		 if resolution_ratio == 1.0:

-			if s_resolution != c_resolution:
-
-				statements.append( 'The files have the same number of pixels but different resolution.' )
-
+			operator = '!='
+			score = 0

		 elif resolution_ratio > 2.0:

-			statements.append( 'This has much higher resolution.' )
-
-			score += duplicate_comparison_score_much_higher_resolution
+			operator = '>>'
+			score = duplicate_comparison_score_much_higher_resolution

-		elif resolution_ratio > 1.0:
+		elif resolution_ratio > 1.00:

-			statements.append( 'This has higher resolution.' )
-
-			score += duplicate_comparison_score_higher_resolution
+			operator = '>'
+			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:
+			operator = '<<'
+			score = -duplicate_comparison_score_much_higher_resolution

-			statements.append( 'This has lower resolution.' )
+		else:

-			score -= duplicate_comparison_score_higher_resolution
+			operator = '<'
+			score = -duplicate_comparison_score_higher_resolution

+		statement = '{} {} {}'.format( lib.Hydrus.Data.ConvertResolutionToPrettyString( s_resolution ), operator, lib.Hydrus.Data.ConvertResolutionToPrettyString( c_resolution ) )
+
+		statements_and_scores[ 'resolution' ] = ( statement, score )
+

	 # same/diff mime

@@ -137,7 +150,10 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):

	 if s_mime != c_mime:

-		statements.append( 'This is ' + HC.mime_string_lookup[ s_mime ] + ', the other is ' + HC.mime_string_lookup[ c_mime ] + '.' )
+		statement = '{} vs {}'.format( HC.mime_string_lookup[ s_mime ], HC.mime_string_lookup[ c_mime ] )
+		score = 0
+
+		statements_and_scores[ 'mime' ] = ( statement, score )

	 # more tags
@@ -145,28 +161,35 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
	 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:

-		if s_num_tags > c_num_tags:
+		if s_num_tags > 0 and c_num_tags > 0:

-			statements.append( 'This has more tags.' )
+			if s_num_tags > c_num_tags:
+
+				operator = '>'
+				score = duplicate_comparison_score_more_tags
+
+			else:
+
+				operator = '<'
+				score = -duplicate_comparison_score_more_tags
+

-			score += duplicate_comparison_score_more_tags
+		elif s_num_tags > 0:

-		elif s_num_tags < c_num_tags:
+			operator = '>>'
+			score = duplicate_comparison_score_more_tags

-			statements.append( 'This has fewer tags.' )
+		elif c_num_tags > 0:

-			score += duplicate_comparison_score_more_tags
+			operator = '<<'
+			score = -duplicate_comparison_score_more_tags

-	elif s_num_tags > 0:
+		statement = '{} tags {} {} tags'.format( lib.Hydrus.Data.ToHumanInt( s_num_tags ), operator, lib.Hydrus.Data.ToHumanInt( c_num_tags ) )

-		statements.append( 'This has tags, the other does not.' )
-
-	elif c_num_tags > 0:
-
-		statements.append( 'This has no tags, the other does.' )
+		statements_and_scores[ 'num_tags' ] = ( statement, score )

	 # older
@@ -174,23 +197,27 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
	 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:
+	one_month = 86400 * 30
+
+	if s_ts is not None and c_ts is not None and abs( s_ts - c_ts ) > one_month:

-		if s_ts < c_ts - 86400 * 30:
+		if s_ts < c_ts:

-			statements.append( 'This is older.' )
+			operator = 'older than'
+			score = duplicate_comparison_score_older

-			score += duplicate_comparison_score_older
-
-		elif c_ts < s_ts - 86400 * 30:
-
-			statements.append( 'This is newer.' )
+		else:

-			score -= duplicate_comparison_score_older
+			operator = 'newer than'
+			score = -duplicate_comparison_score_older

+		statement = '{} {} {}'.format( lib.Hydrus.Data.TimestampToPrettyTimeDelta( s_ts ), operator, lib.Hydrus.Data.TimestampToPrettyTimeDelta( c_ts ) )
+
+		statements_and_scores[ 'time_imported' ] = ( statement, score )
+

-	return ( statements, score )
+	return statements_and_scores

 def MergeTagsManagers( tags_managers ):

diff --git a/include/lib.Networking.Domain.py b/include/lib.Networking.Domain.py
index 164d81b..b7b9a89 100644
--- a/include/lib.Networking.Domain.py
+++ b/include/lib.Networking.Domain.py
@@ -52,7 +52,7 @@ def ConvertDomainIntoSecondLevelDomain( domain ):

	 if len( domains ) == 0:

-		raise lib.Hydrus.Exceptions.URLMatchException( 'That url or domain did not seem to be valid!' )
+		raise lib.Hydrus.Exceptions.URLClassException( 'That url or domain did not seem to be valid!' )

	 return domains[-1]
@@ -166,33 +166,33 @@ def ConvertQueryTextToDict( query_text ):

	 return query_dict

-def ConvertURLMatchesIntoAPIPairs( url_matches ):
+def ConvertURLClassesIntoAPIPairs( url_classes ):

-	url_matches = list( url_matches )
+	url_classes = list( url_classes )

-	NetworkDomainManager.STATICSortURLMatchesDescendingComplexity( url_matches )
+	NetworkDomainManager.STATICSortURLClassesDescendingComplexity( url_classes )

	 pairs = []

-	for url_match in url_matches:
+	for url_class in url_classes:

-		if not url_match.UsesAPIURL():
+		if not url_class.UsesAPIURL():

			 continue

-		api_url = url_match.GetAPIURL( url_match.GetExampleURL() )
+		api_url = url_class.GetAPIURL( url_class.GetExampleURL() )

-		for other_url_match in url_matches:
+		for other_url_class in url_classes:

-			if other_url_match == url_match:
+			if other_url_class == url_class:

				 continue

-			if other_url_match.Matches( api_url ):
+			if other_url_class.Matches( api_url ):

-				pairs.append( ( url_match, other_url_match ) )
+				pairs.append( ( url_class, other_url_class ) )

				 break

@@ -207,12 +207,12 @@ def ConvertURLIntoDomain( url ):

	 if parser_result.scheme == '':

-		raise lib.Hydrus.Exceptions.URLMatchException( 'URL "' + url + '" was not recognised--did you forget the http:// or https://?' )
+		raise lib.Hydrus.Exceptions.URLClassException( 'URL "' + url + '" was not recognised--did you forget the http:// or https://?' )

	 if parser_result.netloc == '':

-		raise lib.Hydrus.Exceptions.URLMatchException( 'URL "' + url + '" was not recognised--is it missing a domain?' )
+		raise lib.Hydrus.Exceptions.URLClassException( 'URL "' + url + '" was not recognised--is it missing a domain?' )

	 domain = parser_result.netloc
@@ -298,7 +298,7 @@ def GetSearchURLs( url ):

				 netloc = ConvertDomainIntoSecondLevelDomain( netloc )

-			except lib.Hydrus.Exceptions.URLMatchException:
+			except lib.Hydrus.Exceptions.URLClassException:

				 continue

@@ -338,7 +338,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
		 self.engine = None

		 self._gugs = lib.Hydrus.Serialisable.SerialisableList()
-		self._url_matches = lib.Hydrus.Serialisable.SerialisableList()
+		self._url_classes = lib.Hydrus.Serialisable.SerialisableList()
		 self._parsers = lib.Hydrus.Serialisable.SerialisableList()
		 self._network_contexts_to_custom_header_dicts = collections.defaultdict( dict )

@@ -346,17 +346,17 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 self._gug_keys_to_display = set()

-		self._url_match_keys_to_display = set()
-		self._url_match_keys_to_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()
+		self._url_class_keys_to_display = set()
+		self._url_class_keys_to_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()

-		self._domains_to_url_matches = collections.defaultdict( list )
+		self._domains_to_url_classes = collections.defaultdict( list )

		 import lib.Actions.Import.Options

		 self._file_post_default_tag_import_options = lib.Actions.Import.Options.TagImportOptions()
		 self._watchable_default_tag_import_options = lib.Actions.Import.Options.TagImportOptions()

-		self._url_match_keys_to_default_tag_import_options = {}
+		self._url_class_keys_to_default_tag_import_options = {}

		 self._gug_keys_to_gugs = {}
		 self._gug_names_to_gugs = {}
@@ -372,31 +372,31 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

	 def _GetDefaultTagImportOptionsForURL( self, url ):

-		url_match = self._GetURLMatch( url )
+		url_class = self._GetURLClass( url )

-		if url_match is None or url_match.GetURLType() not in ( HC.URL_TYPE_POST, HC.URL_TYPE_WATCHABLE ):
+		if url_class is None or url_class.GetURLType() not in ( HC.URL_TYPE_POST, HC.URL_TYPE_WATCHABLE ):

			 return self._file_post_default_tag_import_options

		 try:

-			( url_match, url ) = self._GetNormalisedAPIURLMatchAndURL( url )
+			( url_class, url ) = self._GetNormalisedAPIURLClassAndURL( url )

-		except lib.Hydrus.Exceptions.URLMatchException:
+		except lib.Hydrus.Exceptions.URLClassException:

			 return self._file_post_default_tag_import_options

-		url_match_key = url_match.GetMatchKey()
+		url_class_key = url_class.GetMatchKey()

-		if url_match_key in self._url_match_keys_to_default_tag_import_options:
+		if url_class_key in self._url_class_keys_to_default_tag_import_options:

-			return self._url_match_keys_to_default_tag_import_options[ url_match_key ]
+			return self._url_class_keys_to_default_tag_import_options[ url_class_key ]

		 else:

-			url_type = url_match.GetURLType()
+			url_type = url_class.GetURLType()

			 if url_type == HC.URL_TYPE_POST:

@@ -408,7 +408,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

			 else:

-				raise lib.Hydrus.Exceptions.URLMatchException( 'Could not find tag import options for that kind of URL Class!' )
+				raise lib.Hydrus.Exceptions.URLClassException( 'Could not find tag import options for that kind of URL Class!' )

@@ -431,36 +431,36 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

-	def _GetNormalisedAPIURLMatchAndURL( self, url ):
+	def _GetNormalisedAPIURLClassAndURL( self, url ):

-		url_match = self._GetURLMatch( url )
+		url_class = self._GetURLClass( url )

-		if url_match is None:
+		if url_class is None:

-			raise lib.Hydrus.Exceptions.URLMatchException( 'Could not find a URL Class for ' + url + '!' )
+			raise lib.Hydrus.Exceptions.URLClassException( 'Could not find a URL Class for ' + url + '!' )

-		seen_url_matches = set()
+		seen_url_classes = set()

-		seen_url_matches.add( url_match )
+		seen_url_classes.add( url_class )

-		api_url_match = url_match
+		api_url_class = url_class
		 api_url = url

-		while api_url_match.UsesAPIURL():
+		while api_url_class.UsesAPIURL():

-			api_url = api_url_match.GetAPIURL( api_url )
+			api_url = api_url_class.GetAPIURL( api_url )

-			api_url_match = self._GetURLMatch( api_url )
+			api_url_class = self._GetURLClass( api_url )

-			if api_url_match is None:
+			if api_url_class is None:

-				raise lib.Hydrus.Exceptions.URLMatchException( 'Could not find an API URL Class for ' + api_url + ' URL, which originally came from ' + url + '!' )
+				raise lib.Hydrus.Exceptions.URLClassException( 'Could not find an API URL Class for ' + api_url + ' URL, which originally came from ' + url + '!' )

-			if api_url_match in seen_url_matches:
+			if api_url_class in seen_url_classes:

-				loop_size = len( seen_url_matches )
+				loop_size = len( seen_url_classes )

				 if loop_size == 1:

@@ -475,15 +475,15 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
					 message = 'Could not find an API URL Class for ' + url + ' as it and its API url classes linked in a loop of size ' + lib.Hydrus.Data.ToHumanInt( loop_size ) + '!'

-				raise lib.Hydrus.Exceptions.URLMatchException( message )
+				raise lib.Hydrus.Exceptions.URLClassException( message )

-			seen_url_matches.add( api_url_match )
+			seen_url_classes.add( api_url_class )

-		api_url = api_url_match.Normalise( api_url )
+		api_url = api_url_class.Normalise( api_url )

-		return ( api_url_match, api_url )
+		return ( api_url_class, api_url )

	 def _GetSerialisableInfo( self ):
@@ -491,39 +491,39 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
		 serialisable_gugs = self._gugs.GetSerialisableTuple()
		 serialisable_gug_keys_to_display = [ gug_key.hex() for gug_key in self._gug_keys_to_display ]

-		serialisable_url_matches = self._url_matches.GetSerialisableTuple()
-		serialisable_url_match_keys_to_display = [ url_match_key.hex() for url_match_key in self._url_match_keys_to_display ]
-		serialisable_url_match_keys_to_parser_keys = self._url_match_keys_to_parser_keys.GetSerialisableTuple()
+		serialisable_url_classes = self._url_classes.GetSerialisableTuple()
+		serialisable_url_class_keys_to_display = [ url_class_key.hex() for url_class_key in self._url_class_keys_to_display ]
+		serialisable_url_class_keys_to_parser_keys = self._url_class_keys_to_parser_keys.GetSerialisableTuple()

		 serialisable_file_post_default_tag_import_options = self._file_post_default_tag_import_options.GetSerialisableTuple()
		 serialisable_watchable_default_tag_import_options = self._watchable_default_tag_import_options.GetSerialisableTuple()
-		serialisable_url_match_keys_to_default_tag_import_options = [ ( url_match_key.hex(), tag_import_options.GetSerialisableTuple() ) for ( url_match_key, tag_import_options ) in list(self._url_match_keys_to_default_tag_import_options.items()) ]
+		serialisable_url_class_keys_to_default_tag_import_options = [ ( url_class_key.hex(), tag_import_options.GetSerialisableTuple() ) for ( url_class_key, tag_import_options ) in list(self._url_class_keys_to_default_tag_import_options.items()) ]

-		serialisable_default_tag_import_options_tuple = ( serialisable_file_post_default_tag_import_options, serialisable_watchable_default_tag_import_options, serialisable_url_match_keys_to_default_tag_import_options )
+		serialisable_default_tag_import_options_tuple = ( serialisable_file_post_default_tag_import_options, serialisable_watchable_default_tag_import_options, serialisable_url_class_keys_to_default_tag_import_options )

		 serialisable_parsers = self._parsers.GetSerialisableTuple()
		 serialisable_network_contexts_to_custom_header_dicts = [ ( network_context.GetSerialisableTuple(), list(custom_header_dict.items()) ) for ( network_context, custom_header_dict ) in list(self._network_contexts_to_custom_header_dicts.items()) ]

-		return ( serialisable_gugs, serialisable_gug_keys_to_display, serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsers, serialisable_network_contexts_to_custom_header_dicts )
+		return ( serialisable_gugs, serialisable_gug_keys_to_display, serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsers, serialisable_network_contexts_to_custom_header_dicts )

-	def _GetURLMatch( self, url ):
+	def _GetURLClass( self, url ):

		 domain = ConvertDomainIntoSecondLevelDomain( ConvertURLIntoDomain( url ) )

-		if domain in self._domains_to_url_matches:
+		if domain in self._domains_to_url_classes:

-			url_matches = self._domains_to_url_matches[ domain ]
+			url_classes = self._domains_to_url_classes[ domain ]

-			for url_match in url_matches:
+			for url_class in url_classes:

				 try:

-					url_match.Test( url )
+					url_class.Test( url )

-					return url_match
+					return url_class

-				except lib.Hydrus.Exceptions.URLMatchException:
+				except lib.Hydrus.Exceptions.URLClassException:

					 continue

@@ -537,18 +537,18 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 try:

-			( parser_url_match, parser_url ) = self._GetNormalisedAPIURLMatchAndURL( url )
+			( parser_url_class, parser_url ) = self._GetNormalisedAPIURLClassAndURL( url )

-		except lib.Hydrus.Exceptions.URLMatchException as e:
+		except lib.Hydrus.Exceptions.URLClassException as e:

-			raise lib.Hydrus.Exceptions.URLMatchException( 'Could not find a parser for ' + url + '!' + os.linesep * 2 + str( e ) )
+			raise lib.Hydrus.Exceptions.URLClassException( 'Could not find a parser for ' + url + '!' + os.linesep * 2 + str( e ) )

-		url_match_key = parser_url_match.GetMatchKey()
+		url_class_key = parser_url_class.GetMatchKey()

-		if url_match_key in self._url_match_keys_to_parser_keys:
+		if url_class_key in self._url_class_keys_to_parser_keys:

-			parser_key = self._url_match_keys_to_parser_keys[ url_match_key ]
+			parser_key = self._url_class_keys_to_parser_keys[ url_class_key ]

			 if parser_key is not None and parser_key in self._parser_keys_to_parsers:

@@ -556,28 +556,28 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

-		raise lib.Hydrus.Exceptions.URLMatchException( 'Could not find a parser for ' + parser_url_match.GetName() + ' URL Class!' )
+		raise lib.Hydrus.Exceptions.URLClassException( 'Could not find a parser for ' + parser_url_class.GetName() + ' URL Class!' )

	 def _InitialiseFromSerialisableInfo( self, serialisable_info ):

-		( serialisable_gugs, serialisable_gug_keys_to_display, serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsers, serialisable_network_contexts_to_custom_header_dicts ) = serialisable_info
+		( serialisable_gugs, serialisable_gug_keys_to_display, serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsers, serialisable_network_contexts_to_custom_header_dicts ) = serialisable_info

		 self._gugs = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_gugs )

		 self._gug_keys_to_display = { bytes.fromhex( serialisable_gug_key ) for serialisable_gug_key in serialisable_gug_keys_to_display }

-		self._url_matches = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_matches )
+		self._url_classes = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_classes )

-		self._url_match_keys_to_display = { bytes.fromhex( serialisable_url_match_key ) for serialisable_url_match_key in serialisable_url_match_keys_to_display }
-		self._url_match_keys_to_parser_keys = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_match_keys_to_parser_keys )
+		self._url_class_keys_to_display = { bytes.fromhex( serialisable_url_class_key ) for serialisable_url_class_key in serialisable_url_class_keys_to_display }
+		self._url_class_keys_to_parser_keys = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_class_keys_to_parser_keys )

-		( serialisable_file_post_default_tag_import_options, serialisable_watchable_default_tag_import_options, serialisable_url_match_keys_to_default_tag_import_options ) = serialisable_default_tag_import_options_tuple
+		( serialisable_file_post_default_tag_import_options, serialisable_watchable_default_tag_import_options, serialisable_url_class_keys_to_default_tag_import_options ) = serialisable_default_tag_import_options_tuple

		 self._file_post_default_tag_import_options = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_file_post_default_tag_import_options )
		 self._watchable_default_tag_import_options = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_watchable_default_tag_import_options )

-		self._url_match_keys_to_default_tag_import_options = { bytes.fromhex( serialisable_url_match_key ) : lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_tag_import_options ) for ( serialisable_url_match_key, serialisable_tag_import_options ) in serialisable_url_match_keys_to_default_tag_import_options }
+		self._url_class_keys_to_default_tag_import_options = { bytes.fromhex( serialisable_url_class_key ) : lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_tag_import_options ) for ( serialisable_url_class_key, serialisable_tag_import_options ) in serialisable_url_class_keys_to_default_tag_import_options }

		 self._parsers = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_parsers )

@@ -594,18 +594,18 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

	 def _RecalcCache( self ):

-		self._domains_to_url_matches = collections.defaultdict( list )
+		self._domains_to_url_classes = collections.defaultdict( list )

-		for url_match in self._url_matches:
+		for url_class in self._url_classes:

-			domain = url_match.GetDomain()
+			domain = url_class.GetDomain()

-			self._domains_to_url_matches[ domain ].append( url_match )
+			self._domains_to_url_classes[ domain ].append( url_class )

-		for url_matches in list(self._domains_to_url_matches.values()):
+		for url_classes in list(self._domains_to_url_classes.values()):

-			NetworkDomainManager.STATICSortURLMatchesDescendingComplexity( url_matches )
+			NetworkDomainManager.STATICSortURLClassesDescendingComplexity( url_classes )

		 self._gug_keys_to_gugs = { gug.GetGUGKey() : gug for gug in self._gugs }
@@ -634,120 +634,120 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 if version == 1:

-			( serialisable_url_matches, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info
+			( serialisable_url_classes, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info

-			url_matches = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_matches )
+			url_classes = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_classes )

-			url_match_names_to_display = {}
-			url_match_names_to_page_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()
-			url_match_names_to_gallery_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()
+			url_class_names_to_display = {}
+			url_class_names_to_page_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()
+			url_class_names_to_gallery_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()

-			for url_match in url_matches:
+			for url_class in url_classes:

-				name = url_match.GetName()
+				name = url_class.GetName()

-				if url_match.IsPostURL():
+				if url_class.IsPostURL():

-					url_match_names_to_display[ name ] = True
+					url_class_names_to_display[ name ] = True

-					url_match_names_to_page_parser_keys[ name ] = None
+					url_class_names_to_page_parser_keys[ name ] = None

-				if url_match.IsGalleryURL() or url_match.IsWatchableURL():
+				if url_class.IsGalleryURL() or url_class.IsWatchableURL():

-					url_match_names_to_gallery_parser_keys[ name ] = None
+					url_class_names_to_gallery_parser_keys[ name ] = None

-			serialisable_url_match_names_to_display = list(url_match_names_to_display.items())
-			serialisable_url_match_names_to_page_parser_keys = url_match_names_to_page_parser_keys.GetSerialisableTuple()
-			serialisable_url_match_names_to_gallery_parser_keys = url_match_names_to_gallery_parser_keys.GetSerialisableTuple()
+			serialisable_url_class_names_to_display = list(url_class_names_to_display.items())
+			serialisable_url_class_names_to_page_parser_keys = url_class_names_to_page_parser_keys.GetSerialisableTuple()
+			serialisable_url_class_names_to_gallery_parser_keys = url_class_names_to_gallery_parser_keys.GetSerialisableTuple()

-			new_serialisable_info = ( serialisable_url_matches, serialisable_url_match_names_to_display, serialisable_url_match_names_to_page_parser_keys, serialisable_url_match_names_to_gallery_parser_keys, serialisable_network_contexts_to_custom_header_dicts )
+			new_serialisable_info = ( serialisable_url_classes, serialisable_url_class_names_to_display, serialisable_url_class_names_to_page_parser_keys, serialisable_url_class_names_to_gallery_parser_keys, serialisable_network_contexts_to_custom_header_dicts )

			 return ( 2, new_serialisable_info )

		 if version == 2:

-			( serialisable_url_matches, serialisable_url_match_names_to_display, serialisable_url_match_names_to_page_parser_keys, serialisable_url_match_names_to_gallery_parser_keys, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info
+			( serialisable_url_classes, serialisable_url_class_names_to_display, serialisable_url_class_names_to_page_parser_keys, serialisable_url_class_names_to_gallery_parser_keys, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info

			 parsers = lib.Hydrus.Serialisable.SerialisableList()

			 serialisable_parsing_parsers = parsers.GetSerialisableTuple()

-			url_match_names_to_display = dict( serialisable_url_match_names_to_display )
+			url_class_names_to_display = dict( serialisable_url_class_names_to_display )

-			url_match_keys_to_display = []
+			url_class_keys_to_display = []

-			url_match_names_to_gallery_parser_keys = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_match_names_to_gallery_parser_keys )
-			url_match_names_to_page_parser_keys = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_match_names_to_page_parser_keys )
+			url_class_names_to_gallery_parser_keys = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_class_names_to_gallery_parser_keys )
+			url_class_names_to_page_parser_keys = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_class_names_to_page_parser_keys )

-			url_match_keys_to_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()
+			url_class_keys_to_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()

-			url_matches = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_matches )
+			url_classes = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_url_classes )

-			for url_match in url_matches:
+			for url_class in url_classes:

-				url_match_key = url_match.GetMatchKey()
+				url_class_key = url_class.GetMatchKey()

-				name = url_match.GetName()
+				name = url_class.GetName()

-				if name in url_match_names_to_display and url_match_names_to_display[ name ]:
+				if name in url_class_names_to_display and url_class_names_to_display[ name ]:

-					url_match_keys_to_display.append( url_match_key )
+					url_class_keys_to_display.append( url_class_key )

-			serialisable_url_matches = url_matches.GetSerialisableTuple() # added random key this week, so save these changes back again!
+			serialisable_url_classes = url_classes.GetSerialisableTuple() # added random key this week, so save these changes back again!

-			serialisable_url_match_keys_to_display = [ url_match_key.hex() for url_match_key in url_match_keys_to_display ]
+			serialisable_url_class_keys_to_display = [ url_class_key.hex() for url_class_key in url_class_keys_to_display ]

-			serialisable_url_match_keys_to_parser_keys = url_match_keys_to_parser_keys.GetSerialisableTuple()
+			serialisable_url_class_keys_to_parser_keys = url_class_keys_to_parser_keys.GetSerialisableTuple()

-			new_serialisable_info = ( serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts )
+			new_serialisable_info = ( serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts )

			 return ( 3, new_serialisable_info )

		 if version == 3:

-			( serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info
+			( serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info

			 import lib.Actions.Import.Options

			 self._file_post_default_tag_import_options = lib.Actions.Import.Options.TagImportOptions()
			 self._watchable_default_tag_import_options = lib.Actions.Import.Options.TagImportOptions()

-			self._url_match_keys_to_default_tag_import_options = {}
+			self._url_class_keys_to_default_tag_import_options = {}

			 serialisable_file_post_default_tag_import_options = self._file_post_default_tag_import_options.GetSerialisableTuple()
			 serialisable_watchable_default_tag_import_options = self._watchable_default_tag_import_options.GetSerialisableTuple()
-			serialisable_url_match_keys_to_default_tag_import_options = [ ( url_match_key.hex(), tag_import_options.GetSerialisableTuple() ) for ( url_match_key, tag_import_options ) in list(self._url_match_keys_to_default_tag_import_options.items()) ]
+			serialisable_url_class_keys_to_default_tag_import_options = [ ( url_class_key.hex(), tag_import_options.GetSerialisableTuple() ) for ( url_class_key, tag_import_options ) in list(self._url_class_keys_to_default_tag_import_options.items()) ]

-			serialisable_default_tag_import_options_tuple = ( serialisable_file_post_default_tag_import_options, serialisable_watchable_default_tag_import_options, serialisable_url_match_keys_to_default_tag_import_options )
+			serialisable_default_tag_import_options_tuple = ( serialisable_file_post_default_tag_import_options, serialisable_watchable_default_tag_import_options, serialisable_url_class_keys_to_default_tag_import_options )

-			new_serialisable_info = ( serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts )
+			new_serialisable_info = ( serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts )

			 return ( 4, new_serialisable_info )

		 if version == 4:

-			( serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info
+			( serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info

			 gugs = lib.Hydrus.Serialisable.SerialisableList()

			 serialisable_gugs = gugs.GetSerialisableTuple()

-			new_serialisable_info = ( serialisable_gugs, serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts )
+			new_serialisable_info = ( serialisable_gugs, serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts )

			 return ( 5, new_serialisable_info )

		 if version == 5:

-			( serialisable_gugs, serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info
+			( serialisable_gugs, serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts ) = old_serialisable_info

			 gugs = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_gugs )

@@ -755,7 +755,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

			 serialisable_gug_keys_to_display = [ gug_key.hex() for gug_key in gug_keys_to_display ]

-			new_serialisable_info = ( serialisable_gugs, serialisable_gug_keys_to_display, serialisable_url_matches, serialisable_url_match_keys_to_display, serialisable_url_match_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts )
+			new_serialisable_info = ( serialisable_gugs, serialisable_gug_keys_to_display, serialisable_url_classes, serialisable_url_class_keys_to_display, serialisable_url_class_keys_to_parser_keys, serialisable_default_tag_import_options_tuple, serialisable_parsing_parsers, serialisable_network_contexts_to_custom_header_dicts )

			 return ( 6, new_serialisable_info )

@@ -795,21 +795,21 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
		 self.SetParsers( parsers )

-	def AddURLMatches( self, new_url_matches ):
+	def AddURLClasses( self, new_url_classes ):

		 with self._lock:

-			url_matches = list( self._url_matches )
+			url_classes = list( self._url_classes )

-			for url_match in new_url_matches:
+			for url_class in new_url_classes:

-				url_match.SetNonDupeName( [ u.GetName() for u in url_matches ] )
+				url_class.SetNonDupeName( [ u.GetName() for u in url_classes ] )

-				url_matches.append( url_match )
+				url_classes.append( url_class )

-		self.SetURLMatches( url_matches )
+		self.SetURLClasses( url_classes )

	 def AlreadyHaveExactlyTheseHeaders( self, network_context, headers_list ):
@@ -913,25 +913,25 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
		 return False

-	def AlreadyHaveExactlyThisURLMatch( self, new_url_match ):
+	def AlreadyHaveExactlyThisURLClass( self, new_url_class ):

		 with self._lock:

			 # absent irrelevant variables, do we have the exact same object already in?

-			name = new_url_match.GetName()
-			match_key = new_url_match.GetMatchKey()
-			example_url = new_url_match.GetExampleURL()
+			name = new_url_class.GetName()
+			match_key = new_url_class.GetMatchKey()
+			example_url = new_url_class.GetExampleURL()

-			dupe_url_matches = [ url_match.Duplicate() for url_match in self._url_matches ]
+			dupe_url_classes = [ url_class.Duplicate() for url_class in self._url_classes ]

-			for dupe_url_match in dupe_url_matches:
+			for dupe_url_class in dupe_url_classes:

-				dupe_url_match.SetName( name )
-				dupe_url_match.SetMatchKey( match_key )
-				dupe_url_match.SetExampleURL( example_url )
+				dupe_url_class.SetName( name )
+				dupe_url_class.SetMatchKey( match_key )
+				dupe_url_class.SetExampleURL( example_url )

-				if dupe_url_match.DumpToString() == new_url_match.DumpToString():
+				if dupe_url_class.DumpToString() == new_url_class.DumpToString():

					 return True

@@ -965,11 +965,11 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

-	def AutoAddURLMatchesAndParsers( self, new_url_matches, dupe_url_matches, new_parsers ):
+	def AutoAddURLClassesAndParsers( self, new_url_classes, dupe_url_classes, new_parsers ):

-		for url_match in new_url_matches:
+		for url_class in new_url_classes:

-			url_match.RegenerateMatchKey()
+			url_class.RegenerateMatchKey()

		 for parser in new_parsers:
@@ -985,20 +985,20 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

			 renamees = []

-			for existing_url_match in self._url_matches:
+			for existing_url_class in self._url_classes:

-				if existing_url_match.GetName().startswith( prefix ):
+				if existing_url_class.GetName().startswith( prefix ):

					 continue

-				for new_url_match in new_url_matches:
+				for new_url_class in new_url_classes:

-					if new_url_match.Matches( existing_url_match.GetExampleURL() ) and existing_url_match.Matches( new_url_match.GetExampleURL() ):
+					if new_url_class.Matches( existing_url_class.GetExampleURL() ) and existing_url_class.Matches( new_url_class.GetExampleURL() ):

						 # the url matches match each other, so they are doing the same job

-						renamees.append( existing_url_match )
+						renamees.append( existing_url_class )

						 break

@@ -1008,7 +1008,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

			 for renamee in renamees:

-				existing_names = [ url_match.GetName() for url_match in self._url_matches if url_match != renamee ]
+				existing_names = [ url_class.GetName() for url_class in self._url_classes if url_class != renamee ]

				 renamee.SetName( prefix + renamee.GetName() )

@@ -1016,37 +1016,37 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

-		self.AddURLMatches( new_url_matches )
+		self.AddURLClasses( new_url_classes )
		 self.AddParsers( new_parsers )

		 # we want to match these url matches and parsers together if possible

		 with self._lock:

-			url_matches_to_link = list( new_url_matches )
+			url_classes_to_link = list( new_url_classes )

			 # if downloader adds existing url match but updated parser, we want to update the existing link

-			for dupe_url_match in dupe_url_matches:
+			for dupe_url_class in dupe_url_classes:

				 # this is to make sure we have the right match keys for the link update in a minute

-				actual_existing_dupe_url_match = self._GetURLMatch( dupe_url_match.GetExampleURL() )
+				actual_existing_dupe_url_class = self._GetURLClass( dupe_url_class.GetExampleURL() )

-				if actual_existing_dupe_url_match is not None:
+				if actual_existing_dupe_url_class is not None:

-					url_matches_to_link.append( actual_existing_dupe_url_match )
+					url_classes_to_link.append( actual_existing_dupe_url_class )

-			new_url_match_keys_to_parser_keys = NetworkDomainManager.STATICLinkURLMatchesAndParsers( url_matches_to_link, new_parsers, {} )
+			new_url_class_keys_to_parser_keys = NetworkDomainManager.STATICLinkURLClassesAndParsers( url_classes_to_link, new_parsers, {} )

-			self._url_match_keys_to_parser_keys.update( new_url_match_keys_to_parser_keys )
+			self._url_class_keys_to_parser_keys.update( new_url_class_keys_to_parser_keys )

		 # let's do a trytolink just in case there are loose ends due to some dupe being discarded earlier (e.g. url match is new, but parser was not).

-		self.TryToLinkURLMatchesAndParsers()
+		self.TryToLinkURLClassesAndParsers()

	 def CanValidateInPopup( self, network_contexts ):
@@ -1064,9 +1064,9 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

			 for url in urls:

-				url_match = self._GetURLMatch( url )
+				url_class = self._GetURLClass( url )

-				if url_match is None:
+				if url_class is None:

					 if False:

@@ -1077,13 +1077,13 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

				 else:

-					url_match_key = url_match.GetMatchKey()
+					url_class_key = url_class.GetMatchKey()

-					if url_match_key in self._url_match_keys_to_display:
+					if url_class_key in self._url_class_keys_to_display:

-						url_match_name = url_match.GetName()
+						url_class_name = url_class.GetName()

-						url_tuples.append( ( url_match_name, url ) )
+						url_tuples.append( ( url_class_name, url ) )

@@ -1152,7 +1152,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 with self._lock:

-			return ( self._file_post_default_tag_import_options, self._watchable_default_tag_import_options, self._url_match_keys_to_default_tag_import_options )
+			return ( self._file_post_default_tag_import_options, self._watchable_default_tag_import_options, self._url_class_keys_to_default_tag_import_options )

@@ -1179,7 +1179,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
			 # this might be better as getdownloaderkey, but we'll see how it shakes out
			 # might also be worth being a getifhasdownloader

-			# match the url to a url_match, then lookup that in a 'this downloader can handle this url_match type' dict that we'll manage
+			# match the url to a url_class, then lookup that in a 'this downloader can handle this url_class type' dict that we'll manage

			 pass

@@ -1312,35 +1312,35 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

-	def GetURLMatch( self, url ):
+	def GetURLClass( self, url ):

		 with self._lock:

-			return self._GetURLMatch( url )
+			return self._GetURLClass( url )

-	def GetURLMatches( self ):
+	def GetURLClasses( self ):

		 with self._lock:

-			return list( self._url_matches )
+			return list( self._url_classes )

-	def GetURLMatchKeysToParserKeys( self ):
+	def GetURLClassKeysToParserKeys( self ):

		 with self._lock:

-			return dict( self._url_match_keys_to_parser_keys )
+			return dict( self._url_class_keys_to_parser_keys )

-	def GetURLMatchKeysToDisplay( self ):
+	def GetURLClassKeysToDisplay( self ):

		 with self._lock:

-			return set( self._url_match_keys_to_display )
+			return set( self._url_class_keys_to_display )

@@ -1348,15 +1348,15 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 with self._lock:

-			url_match = self._GetURLMatch( url )
+			url_class = self._GetURLClass( url )

-			if url_match is None:
+			if url_class is None:

				 return ( HC.URL_TYPE_UNKNOWN, 'unknown url', False )

-			url_type = url_match.GetURLType()
-			match_name = url_match.GetName()
+			url_type = url_class.GetURLType()
+			match_name = url_class.GetName()

			 try:

@@ -1364,7 +1364,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

				 can_parse = True

-			except lib.Hydrus.Exceptions.URLMatchException:
+			except lib.Hydrus.Exceptions.URLClassException:

				 can_parse = False

@@ -1383,11 +1383,11 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

				 ( url_to_fetch, parser ) = result

-				url_match = self._GetURLMatch( url )
+				url_class = self._GetURLClass( url )

-				url_name = url_match.GetName()
+				url_name = url_class.GetName()

-				url_to_fetch_match = self._GetURLMatch( url_to_fetch )
+				url_to_fetch_match = self._GetURLClass( url_to_fetch )

				 url_to_fetch_name = url_to_fetch_match.GetName()

@@ -1446,9 +1446,9 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 with self._lock:

-			url_match = self._GetURLMatch( url )
+			url_class = self._GetURLClass( url )

-			if url_match is None:
+			if url_class is None:

				 p = urllib.parse.urlparse( url )

@@ -1465,7 +1465,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

			 else:

-				normalised_url = url_match.Normalise( url )
+				normalised_url = url_class.Normalise( url )

			 return normalised_url
@@ -1516,36 +1516,36 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
		 self.SetParsers( new_parsers )

-	def OverwriteDefaultURLMatches( self, url_match_names ):
+	def OverwriteDefaultURLClasses( self, url_class_names ):

		 with self._lock:

			 import lib.Hydrus.Defaults

-			default_url_matches = lib.Hydrus.Defaults.GetDefaultURLMatches()
+			default_url_classes = lib.Hydrus.Defaults.GetDefaultURLClasses()

-			for url_match in default_url_matches:
+			for url_class in default_url_classes:

-				url_match.RegenerateMatchKey()
+				url_class.RegenerateMatchKey()

-			existing_url_matches = list( self._url_matches )
+			existing_url_classes = list( self._url_classes )

-			new_url_matches = [ url_match for url_match in existing_url_matches if url_match.GetName() not in url_match_names ]
-			new_url_matches.extend( [ url_match for url_match in default_url_matches if url_match.GetName() in url_match_names ] )
+			new_url_classes = [ url_class for url_class in existing_url_classes if url_class.GetName() not in url_class_names ]
+			new_url_classes.extend( [ url_class for url_class in default_url_classes if url_class.GetName() in url_class_names ] )

-		self.SetURLMatches( new_url_matches )
+		self.SetURLClasses( new_url_classes )

-	def OverwriteParserLink( self, url_match, parser ):
+	def OverwriteParserLink( self, url_class, parser ):

		 with self._lock:

-			url_match_key = url_match.GetMatchKey()
+			url_class_key = url_class.GetMatchKey()
			 parser_key = parser.GetParserKey()

-			self._url_match_keys_to_parser_keys[ url_match_key ] = parser_key
+			self._url_class_keys_to_parser_keys[ url_class_key ] = parser_key

@@ -1568,14 +1568,14 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

-	def SetDefaultTagImportOptions( self, file_post_default_tag_import_options, watchable_default_tag_import_options, url_match_keys_to_tag_import_options ):
+	def SetDefaultTagImportOptions( self, file_post_default_tag_import_options, watchable_default_tag_import_options, url_class_keys_to_tag_import_options ):

		 with self._lock:

			 self._file_post_default_tag_import_options = file_post_default_tag_import_options
			 self._watchable_default_tag_import_options = watchable_default_tag_import_options

-			self._url_match_keys_to_default_tag_import_options = url_match_keys_to_tag_import_options
+			self._url_class_keys_to_default_tag_import_options = url_class_keys_to_tag_import_options

			 self._SetDirty()

@@ -1660,19 +1660,19 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

			 parser_keys = { parser.GetParserKey() for parser in parsers }

-			deletee_url_match_keys = set()
+			deletee_url_class_keys = set()

-			for ( url_match_key, parser_key ) in list(self._url_match_keys_to_parser_keys.items()):
+			for ( url_class_key, parser_key ) in list(self._url_class_keys_to_parser_keys.items()):

				 if parser_key not in parser_keys:

-					deletee_url_match_keys.add( url_match_key )
+					deletee_url_class_keys.add( url_class_key )

-			for deletee_url_match_key in deletee_url_match_keys:
+			for deletee_url_class_key in deletee_url_class_keys:

-				del self._url_match_keys_to_parser_keys[ deletee_url_match_key ]
+				del self._url_class_keys_to_parser_keys[ deletee_url_class_key ]

			 #
@@ -1683,51 +1683,51 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

-	def SetURLMatches( self, url_matches ):
+	def SetURLClasses( self, url_classes ):

		 with self._lock:

			 # by default, we will show post urls

-			old_post_url_match_keys = { url_match.GetMatchKey() for url_match in self._url_matches if url_match.IsPostURL() }
-			post_url_match_keys = { url_match.GetMatchKey() for url_match in url_matches if url_match.IsPostURL() }
+			old_post_url_class_keys = { url_class.GetMatchKey() for url_class in self._url_classes if url_class.IsPostURL() }
+			post_url_class_keys = { url_class.GetMatchKey() for url_class in url_classes if url_class.IsPostURL() }

-			added_post_url_match_keys = post_url_match_keys.difference( old_post_url_match_keys )
+			added_post_url_class_keys = post_url_class_keys.difference( old_post_url_class_keys )

-			self._url_match_keys_to_display.update( added_post_url_match_keys )
+			self._url_class_keys_to_display.update( added_post_url_class_keys )

			 #

-			self._url_matches = lib.Hydrus.Serialisable.SerialisableList()
+			self._url_classes = lib.Hydrus.Serialisable.SerialisableList()

-			self._url_matches.extend( url_matches )
+			self._url_classes.extend( url_classes )

-			self._url_matches.sort( key = lambda u: u.GetName() )
+			self._url_classes.sort( key = lambda u: u.GetName() )

			 #

			 # delete orphans

-			url_match_keys = { url_match.GetMatchKey() for url_match in url_matches }
+			url_class_keys = { url_class.GetMatchKey() for url_class in url_classes }

-			self._url_match_keys_to_display.intersection_update( url_match_keys )
+			self._url_class_keys_to_display.intersection_update( url_class_keys )

-			for deletee_key in set( self._url_match_keys_to_parser_keys.keys() ).difference( url_match_keys ):
+			for deletee_key in set( self._url_class_keys_to_parser_keys.keys() ).difference( url_class_keys ):

-				del self._url_match_keys_to_parser_keys[ deletee_key ]
+				del self._url_class_keys_to_parser_keys[ deletee_key ]

			 # any url matches that link to another via the API conversion will not be using parsers

-			url_match_api_pairs = ConvertURLMatchesIntoAPIPairs( self._url_matches )
+			url_class_api_pairs = ConvertURLClassesIntoAPIPairs( self._url_classes )

-			for ( url_match_original, url_match_api ) in url_match_api_pairs:
+			for ( url_class_original, url_class_api ) in url_class_api_pairs:

-				url_match_key = url_match_original.GetMatchKey()
+				url_class_key = url_class_original.GetMatchKey()

-				if url_match_key in self._url_match_keys_to_parser_keys:
+				if url_class_key in self._url_class_keys_to_parser_keys:

-					del self._url_match_keys_to_parser_keys[ url_match_key ]
+					del self._url_class_keys_to_parser_keys[ url_class_key ]

@@ -1737,25 +1737,25 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

-	def SetURLMatchKeysToParserKeys( self, url_match_keys_to_parser_keys ):
+	def SetURLClassKeysToParserKeys( self, url_class_keys_to_parser_keys ):

		 with self._lock:

-			self._url_match_keys_to_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()
+			self._url_class_keys_to_parser_keys = lib.Hydrus.Serialisable.SerialisableBytesDictionary()

-			self._url_match_keys_to_parser_keys.update( url_match_keys_to_parser_keys )
+			self._url_class_keys_to_parser_keys.update( url_class_keys_to_parser_keys )

			 self._SetDirty()

-	def SetURLMatchKeysToDisplay( self, url_match_keys_to_display ):
+	def SetURLClassKeysToDisplay( self, url_class_keys_to_display ):

		 with self._lock:

-			self._url_match_keys_to_display = set()
+			self._url_class_keys_to_display = set()

-			self._url_match_keys_to_display.update( url_match_keys_to_display )
+			self._url_class_keys_to_display.update( url_class_keys_to_display )

			 self._SetDirty()

@@ -1765,24 +1765,24 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 with self._lock:

-			url_match = self._GetURLMatch( url )
+			url_class = self._GetURLClass( url )

-			if url_match is None:
+			if url_class is None:

				 return True

-			return url_match.ShouldAssociateWithFiles()
+			return url_class.ShouldAssociateWithFiles()

-	def TryToLinkURLMatchesAndParsers( self ):
+	def TryToLinkURLClassesAndParsers( self ):

		 with self._lock:

-			new_url_match_keys_to_parser_keys = NetworkDomainManager.STATICLinkURLMatchesAndParsers( self._url_matches, self._parsers, self._url_match_keys_to_parser_keys )
+			new_url_class_keys_to_parser_keys = NetworkDomainManager.STATICLinkURLClassesAndParsers( self._url_classes, self._parsers, self._url_class_keys_to_parser_keys )

-			self._url_match_keys_to_parser_keys.update( new_url_match_keys_to_parser_keys )
+			self._url_class_keys_to_parser_keys.update( new_url_class_keys_to_parser_keys )

			 self._SetDirty()

@@ -1792,14 +1792,14 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 with self._lock:

-			url_match = self._GetURLMatch( url )
+			url_class = self._GetURLClass( url )

-			if url_match is None:
+			if url_class is None:

				 return False

-			return url_match.CanReferToMultipleFiles()
+			return url_class.CanReferToMultipleFiles()

@@ -1807,43 +1807,43 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

		 with self._lock:

-			url_match = self._GetURLMatch( url )
+			url_class = self._GetURLClass( url )

-			if url_match is None:
+			if url_class is None:

				 return False

-			return url_match.RefersToOneFile()
+			return url_class.RefersToOneFile()

	 @staticmethod
-	def STATICLinkURLMatchesAndParsers( url_matches, parsers, existing_url_match_keys_to_parser_keys ):
+	def STATICLinkURLClassesAndParsers( url_classes, parsers, existing_url_class_keys_to_parser_keys ):

-		url_matches = list( url_matches )
+		url_classes = list( url_classes )

-		NetworkDomainManager.STATICSortURLMatchesDescendingComplexity( url_matches )
+		NetworkDomainManager.STATICSortURLClassesDescendingComplexity( url_classes )

		 parsers = list( parsers )

		 parsers.sort( key = lambda p: p.GetName() )

-		new_url_match_keys_to_parser_keys = {}
+		new_url_class_keys_to_parser_keys = {}

-		api_pairs = ConvertURLMatchesIntoAPIPairs( url_matches )
+		api_pairs = ConvertURLClassesIntoAPIPairs( url_classes )

		 # anything that goes to an api url will be parsed by that api's parser--it can't have its own
-		api_pair_unparsable_url_matches = set()
+		api_pair_unparsable_url_classes = set()

		 for ( a, b ) in api_pairs:

-			api_pair_unparsable_url_matches.add( a )
+			api_pair_unparsable_url_classes.add( a )

		 #

-		# I have to do this backwards, going through parsers and then url_matches, so I can do a proper url match lookup like the real domain manager does it
+		# I have to do this backwards, going through parsers and then url_classes, so I can do a proper url match lookup like the real domain manager does it
		 # otherwise, if we iterate through url matches looking for parsers to match them, we have gallery url matches thinking they match parser post urls
		 # e.g.
		 # The page parser might say it supports https://danbooru.donmai.us/posts/3198277
@@ -1857,20 +1857,20 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

			 for example_url in example_urls:

-				for url_match in url_matches:
+				for url_class in url_classes:

-					if url_match.Matches( example_url ):
+					if url_class.Matches( example_url ):

						 # we have a match. this is the 'correct' match for this example url, and we should not search any more, so we break below

-						url_match_key = url_match.GetMatchKey()
+						url_class_key = url_class.GetMatchKey()

-						parsable = url_match.IsParsable()
-						linkable = url_match_key not in existing_url_match_keys_to_parser_keys and url_match_key not in new_url_match_keys_to_parser_keys
+						parsable = url_class.IsParsable()
+						linkable = url_class_key not in existing_url_class_keys_to_parser_keys and url_class_key not in new_url_class_keys_to_parser_keys

						 if parsable and linkable:

-							new_url_match_keys_to_parser_keys[ url_match_key ] = parser.GetParserKey()
+							new_url_class_keys_to_parser_keys[ url_class_key ] = parser.GetParserKey()

						 break
@@ -1881,16 +1881,16 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
		 '''
		 #

-		for url_match in url_matches:
+		for url_class in url_classes:

-			if not url_match.IsParsable() or url_match in api_pair_unparsable_url_matches:
+			if not url_class.IsParsable() or url_class in api_pair_unparsable_url_classes:

				 continue

-			url_match_key = url_match.GetMatchKey()
+			url_class_key = url_class.GetMatchKey()

-			if url_match_key in existing_url_match_keys_to_parser_keys:
+			if url_class_key in existing_url_class_keys_to_parser_keys:

				 continue

@@ -1899,20 +1899,20 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):

				 example_urls = parser.GetExampleURLs()

-				if True in ( url_match.Matches( example_url ) for example_url in example_urls ):
+				if True in ( url_class.Matches( example_url ) for example_url in example_urls ):

-					new_url_match_keys_to_parser_keys[ url_match_key ] = parser.GetParserKey()
+					new_url_class_keys_to_parser_keys[ url_class_key ] = parser.GetParserKey()

					 break

		 '''
-		return new_url_match_keys_to_parser_keys
+		return new_url_class_keys_to_parser_keys

	 @staticmethod
-	def STATICSortURLMatchesDescendingComplexity( url_matches ):
+	def STATICSortURLClassesDescendingComplexity( url_classes ):

		 # we sort them in descending complexity so that
		 # post url/manga subpage
@@ -1947,7 +1947,7 @@ class NetworkDomainManager( lib.Hydrus.Serialisable.SerialisableBase ):
			 return ( u_t_precision_value, u_e.count( '/' ), u_e.count( '=' ) )

-		url_matches.sort( key = key, reverse = True )
+		url_classes.sort( key = key, reverse = True )

 lib.Hydrus.Serialisable.HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ lib.Hydrus.Serialisable.HG.SerialisableType["NETWORK_DOMAIN_MANAGER"] ] = NetworkDomainManager
@@ -2511,17 +2511,17 @@ class NestedGalleryURLGenerator( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

 lib.Hydrus.Serialisable.HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ lib.Hydrus.Serialisable.HG.SerialisableType["NESTED_GALLERY_URL_GENERATOR"] ] = NestedGalleryURLGenerator

-class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):
+class URLClass( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

-	SERIALISABLE_TYPE_NUM = lib.Hydrus.Serialisable.HG.SerialisableType["URL_MATCH"]
+	SERIALISABLE_TYPE_NUM = lib.Hydrus.Serialisable.HG.SerialisableType["URL_CLASS"]
	 SERIALISABLE_NAME = 'URL Class'
	 SERIALISABLE_VERSION = 6

-	def __init__( self, name, url_match_key = None, url_type = None, preferred_scheme = 'https', netloc = 'hostname.com', match_subdomains = False, keep_matched_subdomains = False, path_components = None, parameters = None, api_lookup_converter = None, can_produce_multiple_files = False, should_be_associated_with_files = True, gallery_index_type = None, gallery_index_identifier = None, gallery_index_delta = 1, example_url = 'https://hostname.com/post/page.php?id=123456&s=view' ):
+	def __init__( self, name, url_class_key = None, url_type = None, preferred_scheme = 'https', netloc = 'hostname.com', match_subdomains = False, keep_matched_subdomains = False, path_components = None, parameters = None, api_lookup_converter = None, can_produce_multiple_files = False, should_be_associated_with_files = True, gallery_index_type = None, gallery_index_identifier = None, gallery_index_delta = 1, example_url = 'https://hostname.com/post/page.php?id=123456&s=view' ):

-		if url_match_key is None:
+		if url_class_key is None:

-			url_match_key = lib.Hydrus.Data.GenerateKey()
+			url_class_key = lib.Hydrus.Data.GenerateKey()

		 if url_type is None:
@@ -2558,7 +2558,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

		 lib.Hydrus.Serialisable.SerialisableBaseNamed.__init__( self, name )

-		self._url_match_key = url_match_key
+		self._url_class_key = url_class_key
		 self._url_type = url_type
		 self._preferred_scheme = preferred_scheme
		 self._netloc = netloc
@@ -2629,7 +2629,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

				 else:

-					raise lib.Hydrus.Exceptions.URLMatchException( 'Could not clip path--given url appeared to be too short!' )
+					raise lib.Hydrus.Exceptions.URLClassException( 'Could not clip path--given url appeared to be too short!' )

				 clipped_path_components.append( clipped_path_component )
@@ -2662,7 +2662,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

				 if default is None:

-					raise lib.Hydrus.Exceptions.URLMatchException( 'Could not flesh out query--no default for ' + key + ' defined!' )
+					raise lib.Hydrus.Exceptions.URLClassException( 'Could not flesh out query--no default for ' + key + ' defined!' )

				 else:

@@ -2678,19 +2678,19 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

	 def _GetSerialisableInfo( self ):

-		serialisable_url_match_key = self._url_match_key.hex()
+		serialisable_url_class_key = self._url_class_key.hex()
		 serialisable_path_components = [ ( string_match.GetSerialisableTuple(), default ) for ( string_match, default ) in self._path_components ]
		 serialisable_parameters = [ ( key, ( string_match.GetSerialisableTuple(), default ) ) for ( key, ( string_match, default ) ) in list(self._parameters.items()) ]
		 serialisable_api_lookup_converter = self._api_lookup_converter.GetSerialisableTuple()

-		return ( serialisable_url_match_key, self._url_type, self._preferred_scheme, self._netloc, self._match_subdomains, self._keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, self._can_produce_multiple_files, self._should_be_associated_with_files, self._gallery_index_type, self._gallery_index_identifier, self._gallery_index_delta, self._example_url )
+		return ( serialisable_url_class_key, self._url_type, self._preferred_scheme, self._netloc, self._match_subdomains, self._keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, self._can_produce_multiple_files, self._should_be_associated_with_files, self._gallery_index_type, self._gallery_index_identifier, self._gallery_index_delta, self._example_url )

	 def _InitialiseFromSerialisableInfo( self, serialisable_info ):

-		( serialisable_url_match_key, self._url_type, self._preferred_scheme, self._netloc, self._match_subdomains, self._keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, self._can_produce_multiple_files, self._should_be_associated_with_files, self._gallery_index_type, self._gallery_index_identifier, self._gallery_index_delta, self._example_url ) = serialisable_info
+		( serialisable_url_class_key, self._url_type, self._preferred_scheme, self._netloc, self._match_subdomains, self._keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, self._can_produce_multiple_files, self._should_be_associated_with_files, self._gallery_index_type, self._gallery_index_identifier, self._gallery_index_delta, self._example_url ) = serialisable_info

-		self._url_match_key = bytes.fromhex( serialisable_url_match_key )
+		self._url_class_key = bytes.fromhex( serialisable_url_class_key )
		 self._path_components = [ ( lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_string_match ), default ) for ( serialisable_string_match, default ) in serialisable_path_components ]
		 self._parameters = { key : ( lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_string_match ), default ) for ( key, ( serialisable_string_match, default ) ) in serialisable_parameters }
		 self._api_lookup_converter = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_api_lookup_converter )
@@ -2702,22 +2702,22 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

			 ( url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, example_url ) = old_serialisable_info

-			url_match_key = lib.Hydrus.Data.GenerateKey()
+			url_class_key = lib.Hydrus.Data.GenerateKey()

-			serialisable_url_match_key = url_match_key.hex()
+			serialisable_url_class_key = url_class_key.hex()

			 api_lookup_converter = lib.Handling.Parsing.StringConverter( example_string = example_url )

			 serialisable_api_lookup_converter = api_lookup_converter.GetSerialisableTuple()

-			new_serialisable_info = ( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, example_url )
+			new_serialisable_info = ( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, example_url )

			 return ( 2, new_serialisable_info )

		 if version == 2:

-			( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, example_url ) = old_serialisable_info
+			( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, example_url ) = old_serialisable_info

			 if url_type in ( HC.URL_TYPE_FILE, HC.URL_TYPE_POST ):

@@ -2728,38 +2728,38 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):
				 should_be_associated_with_files = False

-			new_serialisable_info = ( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, should_be_associated_with_files, example_url )
+			new_serialisable_info = ( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, should_be_associated_with_files, example_url )

			 return ( 3, new_serialisable_info )

		 if version == 3:

-			( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, should_be_associated_with_files, example_url ) = old_serialisable_info
+			( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, should_be_associated_with_files, example_url ) = old_serialisable_info

			 can_produce_multiple_files = False

-			new_serialisable_info = ( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, example_url )
+			new_serialisable_info = ( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, example_url )

			 return ( 4, new_serialisable_info )

		 if version == 4:

-			( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, example_url ) = old_serialisable_info
+			( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, example_url ) = old_serialisable_info

			 gallery_index_type = None
			 gallery_index_identifier = None
			 gallery_index_delta = 1

-			new_serialisable_info = ( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, gallery_index_type, gallery_index_identifier, gallery_index_delta, example_url )
+			new_serialisable_info = ( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, gallery_index_type, gallery_index_identifier, gallery_index_delta, example_url )

			 return ( 5, new_serialisable_info )

		 if version == 5:

-			( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, gallery_index_type, gallery_index_identifier, gallery_index_delta, example_url ) = old_serialisable_info
+			( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, gallery_index_type, gallery_index_identifier, gallery_index_delta, example_url ) = old_serialisable_info

			 path_components = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_path_components )
			 parameters = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_parameters )
@@ -2770,7 +2770,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):
			 serialisable_path_components = [ ( string_match.GetSerialisableTuple(), default ) for ( string_match, default ) in path_components ]
			 serialisable_parameters = [ ( key, ( string_match.GetSerialisableTuple(), default ) ) for ( key, ( string_match, default ) ) in list(parameters.items()) ]

-			new_serialisable_info = ( serialisable_url_match_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, gallery_index_type, gallery_index_identifier, gallery_index_delta, example_url )
+			new_serialisable_info = ( serialisable_url_class_key, url_type, preferred_scheme, netloc, match_subdomains, keep_matched_subdomains, serialisable_path_components, serialisable_parameters, serialisable_api_lookup_converter, can_produce_multiple_files, should_be_associated_with_files, gallery_index_type, gallery_index_identifier, gallery_index_delta, example_url )

			 return ( 6, new_serialisable_info )

@@ -2832,7 +2832,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

	 def GetMatchKey( self ):

-		return self._url_match_key
+		return self._url_class_key

	 def GetNextGalleryPage( self, url ):
@@ -2865,7 +2865,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

			 except IndexError:

-				raise lib.Hydrus.Exceptions.URLMatchException( 'Could not generate next gallery page--not enough path components!' )
+				raise lib.Hydrus.Exceptions.URLClassException( 'Could not generate next gallery page--not enough path components!' )

			 try:
@@ -2874,7 +2874,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

			 except Exception:

-				raise lib.Hydrus.Exceptions.URLMatchException( 'Could not generate next gallery page--index component was not an integer!' )
+				raise lib.Hydrus.Exceptions.URLClassException( 'Could not generate next gallery page--index component was not an integer!' )

			 path_components[ page_index_path_component_index ] = str( page_index + self._gallery_index_delta )
@@ -2889,7 +2889,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

			 if page_index_name not in query_dict:

-				raise lib.Hydrus.Exceptions.URLMatchException( 'Could not generate next gallery page--did not find ' + str( self._gallery_index_identifier ) + ' in parameters!' )
+				raise lib.Hydrus.Exceptions.URLClassException( 'Could not generate next gallery page--did not find ' + str( self._gallery_index_identifier ) + ' in parameters!' )

			 page_index = query_dict[ page_index_name ]
@@ -2900,7 +2900,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

			 except Exception:

-				raise lib.Hydrus.Exceptions.URLMatchException( 'Could not generate next gallery page--index component was not an integer!' )
+				raise lib.Hydrus.Exceptions.URLClassException( 'Could not generate next gallery page--index component was not an integer!' )

			 query_dict[ page_index_name ] = page_index + self._gallery_index_delta
@@ -2955,7 +2955,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

			 return True

-		except lib.Hydrus.Exceptions.URLMatchException:
+		except lib.Hydrus.Exceptions.URLClassException:

			 return False

@@ -2998,7 +2998,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

	 def RegenerateMatchKey( self ):

-		self._url_match_key = lib.Hydrus.Data.GenerateKey()
+		self._url_class_key = lib.Hydrus.Data.GenerateKey()

	 def SetExampleURL( self, example_url ):
@@ -3008,7 +3008,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

	 def SetMatchKey( self, match_key ):

-		self._url_match_key = match_key
+		self._url_class_key = match_key

	 def ShouldAssociateWithFiles( self ):
@@ -3024,14 +3024,14 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

			 if p.netloc != self._netloc and not p.netloc.endswith( '.' + self._netloc ):

-				raise lib.Hydrus.Exceptions.URLMatchException( p.netloc + ' (potentially excluding subdomains) did not match ' + self._netloc )
+				raise lib.Hydrus.Exceptions.URLClassException( p.netloc + ' (potentially excluding subdomains) did not match ' + self._netloc )

		 else:

			 if not DomainEqualsAnotherForgivingWWW( p.netloc, self._netloc ):

-				raise lib.Hydrus.Exceptions.URLMatchException( p.netloc + ' did not match ' + self._netloc )
+				raise lib.Hydrus.Exceptions.URLClassException( p.netloc + ' did not match ' + self._netloc )

@@ -3056,12 +3056,12 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

				 except lib.Hydrus.Exceptions.StringMatchException as e:

-					raise lib.Hydrus.Exceptions.URLMatchException( str( e ) )
+					raise lib.Hydrus.Exceptions.URLClassException( str( e ) )

			 elif default is None:

-				raise lib.Hydrus.Exceptions.URLMatchException( url_path + ' did not have enough of the required path components!' )
+				raise lib.Hydrus.Exceptions.URLClassException( url_path + ' did not have enough of the required path components!' )

@@ -3073,7 +3073,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

				 if default is None:

-					raise lib.Hydrus.Exceptions.URLMatchException( key + ' not found in ' + p.query )
+					raise lib.Hydrus.Exceptions.URLClassException( key + ' not found in ' + p.query )

				 else:

@@ -3089,7 +3089,7 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):

			 except lib.Hydrus.Exceptions.StringMatchException as e:

-				raise lib.Hydrus.Exceptions.URLMatchException( str( e ) )
+				raise lib.Hydrus.Exceptions.URLClassException( str( e ) )

@@ -3104,4 +3104,4 @@ class URLMatch( lib.Hydrus.Serialisable.SerialisableBaseNamed ):
		 return self._api_lookup_converter.MakesChanges()

-lib.Hydrus.Serialisable.HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ lib.Hydrus.Serialisable.HG.SerialisableType["URL_MATCH"] ] = URLMatch
+lib.Hydrus.Serialisable.HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ lib.Hydrus.Serialisable.HG.SerialisableType["URL_CLASS"] ] = URLClass
diff --git a/include/lib.GUI.Rendering.py b/include/lib.GUI.Rendering.py
index bf2cecc..f6cc5a9 100644
--- a/include/lib.GUI.Rendering.py
+++ b/include/lib.GUI.Rendering.py
@@ -50,7 +50,7 @@ def FrameIndexOutOfRange( index, range_start, range_end ):

 def GenerateHydrusBitmap( path, mime, compressed = True ):

-	numpy_image = lib.Handling.Image.Local.GenerateNumpyImage( path, mime )
+	numpy_image = lib.Handling.Image.Local.GenerateNumPyImage( path, mime )

	 return GenerateHydrusBitmapFromNumPyImage( numpy_image, compressed = compressed )

@@ -94,7 +94,7 @@ class ImageRenderer( object ):

	 def _Initialise( self ):

-		self._numpy_image = lib.Handling.Image.Local.GenerateNumpyImage( self._path, self._mime )
+		self._numpy_image = lib.Handling.Image.Local.GenerateNumPyImage( self._path, self._mime )

	 def GetEstimatedMemoryFootprint( self ):
@@ -129,7 +129,7 @@ class ImageRenderer( object ):

		 else:

-			wx_numpy_image = lib.Handling.Image.Local.ResizeNumpyImageForMediaViewer( self._media.GetMime(), self._numpy_image, target_resolution )
+			wx_numpy_image = lib.Handling.Image.Local.ResizeNumPyImageForMediaViewer( self._media.GetMime(), self._numpy_image, target_resolution )

		 ( wx_height, wx_width, wx_depth ) = wx_numpy_image.shape
diff --git a/include/lib.Hydrus.Search.py b/include/lib.Hydrus.Search.py
index ff2db59..4cef7ce 100644
--- a/include/lib.Hydrus.Search.py
+++ b/include/lib.Hydrus.Search.py
@@ -804,7 +804,7 @@ class Predicate( lib.Hydrus.Serialisable.SerialisableBase ):

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

-			if rule_type == 'url_match':
+			if rule_type in ( 'url_match', 'url_class' ):

				 serialisable_rule = rule.GetSerialisableTuple()

@@ -855,7 +855,7 @@ class Predicate( lib.Hydrus.Serialisable.SerialisableBase ):

			 ( operator, rule_type, serialisable_rule, description ) = serialisable_value

-			if rule_type == 'url_match':
+			if rule_type in ( 'url_match', 'url_class' ):

				 rule = lib.Hydrus.Serialisable.CreateFromSerialisableTuple( serialisable_rule )

diff --git a/include/lib.Hydrus.Serialisable.Local.py b/include/lib.Hydrus.Serialisable.Local.py
index 4c88ebc..5495eff 100644
--- a/include/lib.Hydrus.Serialisable.Local.py
+++ b/include/lib.Hydrus.Serialisable.Local.py
@@ -25,13 +25,6 @@ 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
-
 def CreateTopImage( width, title, payload_description, text ):

	 text_extent_bmp = HG.client_controller.bitmap_manager.GetBitmap( 20, 20, 24 )
diff --git a/include/lib.Handling.Video.Local.py b/include/lib.Handling.Video.Local.py
index 48dd38e..32e69e3 100644
--- a/include/lib.Handling.Video.Local.py
+++ b/include/lib.Handling.Video.Local.py
@@ -44,16 +44,6 @@ def GetCVVideoProperties( path ):

	 return ( ( width, height ), duration, num_frames )

-def GetVideoFrameDuration( path ):
-
-	cv_video = cv2.VideoCapture( path )
-
-	fps = cv_video.get( CAP_PROP_FPS )
-
-	if fps in ( 0, 1000 ):
		raise lib.Hydrus.Exceptions.CantRenderWithCVException()
-
-	return 1000.0 / fps
-
 # the cv code was initially written by @fluffy_cub
 class GIFRenderer( object ):

@@ -113,7 +103,7 @@ class GIFRenderer( object ):
				 self._pil_canvas = current_frame

-			numpy_image = lib.Handling.Image.Local.GenerateNumPyImageFromPILImage( self._pil_canvas )
+			numpy_image = lib.Handling.Image.GenerateNumPyImageFromPILImage( self._pil_canvas )

		 self._next_render_index = ( self._next_render_index + 1 ) % self._num_frames
@@ -195,7 +185,7 @@ class GIFRenderer( object ):

				 numpy_image = self._GetCurrentFrame()

-				numpy_image = lib.Handling.Image.Local.ResizeNumpyImage( numpy_image, self._target_resolution )
+				numpy_image = lib.Handling.Image.ResizeNumPyImage( numpy_image, self._target_resolution )

				 numpy_image = cv2.cvtColor( numpy_image, cv2.COLOR_BGR2RGB )

@@ -222,7 +212,7 @@ class GIFRenderer( object ):

			 numpy_image = self._GetCurrentFrame()

-			numpy_image = lib.Handling.Image.Local.ResizeNumpyImage( numpy_image, self._target_resolution )
+			numpy_image = lib.Handling.Image.ResizeNumPyImage( numpy_image, self._target_resolution )

		 self._last_frame = numpy_image
diff --git a/include/HydrusConstants.py b/include/HydrusConstants.py
index 30262a7..d5d30bb 100755
--- a/include/HydrusConstants.py
+++ b/include/HydrusConstants.py
@@ -67,7 +67,7 @@ options = {}
 # Misc

 NETWORK_VERSION = 18
-SOFTWARE_VERSION = 350
+SOFTWARE_VERSION = 351
 CLIENT_API_VERSION = 6

 SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/include/lib.Hydrus.DB.py b/include/lib.Hydrus.DB.py
index 0524057..435575f 100644
--- a/include/lib.Hydrus.DB.py
+++ b/include/lib.Hydrus.DB.py
@@ -671,8 +671,8 @@ class HydrusDB( object ):
		 # blah_id IN ( ?, ?, ? ) is fast and cacheable but there's a small limit (1024 is too many) to the number of params sql can handle
		 # so lets do the latter but break it into 256-strong chunks to get a good medium

-		# this will take a select statement with %s like so:
-		# SELECT blah_id, blah FROM blahs WHERE blah_id IN %s;
+		# this will take a select statement with {} like so:
+		# SELECT blah_id, blah FROM blahs WHERE blah_id IN {};

		 MAX_CHUNK_SIZE = 256

@@ -680,7 +680,7 @@ class HydrusDB( object ):
		 # and also so we aren't overmaking it when this gets spammed with a lot of len() == 1 calls
		 if len( xs ) >= MAX_CHUNK_SIZE:

-			max_statement = select_statement % ( '(' + ','.join( '?' * MAX_CHUNK_SIZE ) + ')' )
+			max_statement = select_statement.format( '({})'.format( ','.join( '?' * MAX_CHUNK_SIZE ) ) )

		 for chunk in lib.Hydrus.Data.SplitListIntoChunks( xs, MAX_CHUNK_SIZE ):
@@ -691,7 +691,7 @@ class HydrusDB( object ):

			 else:

-				chunk_statement = select_statement % ( '(' + ','.join( '?' * len( chunk ) ) + ')' )
+				chunk_statement = select_statement.format( '({})'.format( ','.join( '?' * len( chunk ) ) ) )

			 for row in self._c.execute( chunk_statement, chunk ):
diff --git a/include/lib.Hydrus.Data.py b/include/lib.Hydrus.Data.py
index af096ef..8a8795e 100644
--- a/include/lib.Hydrus.Data.py
+++ b/include/lib.Hydrus.Data.py
@@ -75,11 +75,6 @@ def ConvertFloatToPercentage( f ):

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

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

	 if i == 1:

	 	return 'pixels'
diff --git a/include/lib.Hydrus.Exceptions.py b/include/lib.Hydrus.Exceptions.py
index 70bb909..b5d3550 100644
--- a/include/lib.Hydrus.Exceptions.py
+++ b/include/lib.Hydrus.Exceptions.py
@@ -28,7 +28,7 @@ class DecompressionBombException( SizeException ): pass
 class ParseException( HydrusExceptionBase ): pass
 class StringConvertException( ParseException ): pass
 class StringMatchException( ParseException ): pass
-class URLMatchException( ParseException ): pass
+class URLClassException( ParseException ): pass
 class GUGException( ParseException ): pass

 class NetworkException( HydrusExceptionBase ): pass
diff --git a/include/lib.Handling.py b/include/lib.Handling.py
index 3af0619..b0f4c74 100644
--- a/include/lib.Handling.py
+++ b/include/lib.Handling.py
@@ -13,7 +13,6 @@ import lib.Handling.Video
 import os
 import threading
 import traceback
-import io

 # Mime

@@ -53,37 +52,14 @@ header_and_mime = [
	 ( 0, b'\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C', HC.UNDETERMINED_WM )
	 ]

-def SaveThumbnailToStreamPIL( pil_image, bounding_dimensions, f ):
-
-	# when the palette is limited, the thumbnail antialias won't add new colours, so you get nearest-neighbour-like behaviour
-
-	original_file_was_png = pil_image.format == 'PNG'
-
-	pil_image = lib.Handling.Image.Dequantize( pil_image )
-
-	thumbnail_dimensions = lib.Handling.Image.GetThumbnailResolution( pil_image.size, bounding_dimensions )
-
-	lib.Handling.Image.ResizePILImage( pil_image, thumbnail_dimensions )
-
-	if original_file_was_png or pil_image.mode == 'RGBA':
-
-		pil_image.save( f, 'PNG' )
-
-	else:
-
-		pil_image.save( f, 'JPEG', quality = 92 )
-
-
-def GenerateThumbnailBytes( path, bounding_dimensions, mime, width, height, duration, num_frames, percentage_in = 35 ):
+def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames, percentage_in = 35 ):

	 if mime in ( HC.IMAGE_JPEG, HC.IMAGE_PNG, HC.IMAGE_GIF, HC.IMAGE_WEBP, HC.IMAGE_TIFF ):

-		thumbnail_bytes = GenerateThumbnailBytesFromStaticImagePath( path, bounding_dimensions, mime )
+		thumbnail_bytes = lib.Handling.Image.GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime )

	 else:

-		f = io.BytesIO()
-
		 if mime == HC.APPLICATION_FLASH:

			 ( os_file_handle, temp_path ) = lib.Hydrus.Paths.GetTempPath()
@@ -92,30 +68,22 @@ def GenerateThumbnailBytes( path, bounding_dimensions, mime, width, height, dura

				 lib.Handling.Flash.RenderPageToFile( path, temp_path, 1 )

-				pil_image = lib.Handling.Image.GeneratePILImage( temp_path )
-
-				SaveThumbnailToStreamPIL( pil_image, bounding_dimensions, f )
+				thumbnail_bytes = lib.Handling.Image.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, mime )

			 except Exception:

-				flash_default_path = os.path.join( HC.STATIC_DIR, 'flash.png' )
+				thumb_path = os.path.join( HC.STATIC_DIR, 'flash.png' )

-				pil_image = lib.Handling.Image.GeneratePILImage( flash_default_path )
-
-				SaveThumbnailToStreamPIL( pil_image, bounding_dimensions, f )
+				thumbnail_bytes = lib.Handling.Image.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, mime )

			 finally:

-				del pil_image
-
				 lib.Hydrus.Paths.CleanUpTempPath( os_file_handle, temp_path )

		 else:

-			cropped_dimensions = lib.Handling.Image.GetThumbnailResolution( ( width, height ), bounding_dimensions )
-
-			renderer = lib.Handling.Video.VideoRendererFFMPEG( path, mime, duration, num_frames, cropped_dimensions )
+			renderer = lib.Handling.Video.VideoRendererFFMPEG( path, mime, duration, num_frames, target_resolution )

			 renderer.read_frame() # this initialises the renderer and loads the first frame as a fallback

@@ -130,46 +98,18 @@ def GenerateThumbnailBytes( path, bounding_dimensions, mime, width, height, dura
				 raise Exception( 'Could not create a thumbnail from that video!' )

-			pil_image = lib.Handling.Image.GeneratePILImageFromNumpyImage( numpy_image )
+			numpy_image = lib.Handling.Image.ResizeNumPyImage( numpy_image, target_resolution ) # just in case ffmpeg doesn't deliver right

-			SaveThumbnailToStreamPIL( pil_image, bounding_dimensions, f )
+			thumbnail_bytes = lib.Handling.Image.GenerateThumbnailBytesNumPy( numpy_image, mime )

			 renderer.Stop()

			 del renderer

-		f.seek( 0 )
-
-		thumbnail_bytes = f.read()
-
-		f.close()
-
-
-	return thumbnail_bytes
-
-def GenerateThumbnailBytesFromPIL( pil_image, bounding_dimensions, mime ):
-
-	f = io.BytesIO()
-
-	SaveThumbnailToStreamPIL( pil_image, bounding_dimensions, f )
-
-	f.seek( 0 )
-
-	thumbnail_bytes = f.read()
-
-	f.close()

	 return thumbnail_bytes

-def GenerateThumbnailBytesFromStaticImagePathPIL( path, bounding_dimensions, mime ):
-
-	pil_image = lib.Handling.Image.GeneratePILImage( path )
-
-	return GenerateThumbnailBytesFromPIL( pil_image, bounding_dimensions, mime )
-
-GenerateThumbnailBytesFromStaticImagePath = GenerateThumbnailBytesFromStaticImagePathPIL
-
 def GetExtraHashesFromPath( path ):

	 h_md5 = hashlib.md5()
diff --git a/include/lib.Handling.Image.py b/include/lib.Handling.Image.py
index 4d142ce..efbcd45 100755
--- a/include/lib.Handling.Image.py
+++ b/include/lib.Handling.Image.py
@@ -1,6 +1,9 @@
 import lib.Hydrus.Constants as HC
 import lib.Hydrus.Exceptions
 import lib.Hydrus.Threading
+import io
+import numpy
+import numpy.core.multiarray # important this comes before cv!
 import os
 from PIL import _imaging
 from PIL import ImageFile as PILImageFile
@@ -36,6 +39,37 @@ warnings.simplefilter( 'ignore', PILImage.DecompressionBombWarning )
 OLD_PIL_MAX_IMAGE_PIXELS = PILImage.MAX_IMAGE_PIXELS
 PILImage.MAX_IMAGE_PIXELS = None # this turns off decomp check entirely, wew

+try:
+
+	import cv2
+
+	if cv2.__version__.startswith( '2' ):
+
+		CV_IMREAD_FLAGS_SUPPORTS_ALPHA = cv2.CV_LOAD_IMAGE_UNCHANGED
+		CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = CV_IMREAD_FLAGS_SUPPORTS_ALPHA
+
+		# there's something wrong with these, but I don't have an easy test env for it atm
+		# CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = cv2.CV_LOAD_IMAGE_ANYDEPTH | cv2.CV_LOAD_IMAGE_ANYCOLOR
+
+		CV_JPEG_THUMBNAIL_ENCODE_PARAMS = []
+		CV_PNG_THUMBNAIL_ENCODE_PARAMS = []
+
+	else:
+
+		CV_IMREAD_FLAGS_SUPPORTS_ALPHA = cv2.IMREAD_UNCHANGED
+		CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = cv2.IMREAD_ANYDEPTH | cv2.IMREAD_ANYCOLOR # this preserves colour info but does EXIF reorientation and flipping
+
+		CV_JPEG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_JPEG_QUALITY, 92 ]
+		CV_PNG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_PNG_COMPRESSION, 9 ]
+
+
+	OPENCV_OK = True
+
+except Exception:
+
+	OPENCV_OK = False
+
+
 def ConvertToPngIfBmp( path ):

	 with open( path, 'rb' ) as f:
@@ -83,6 +117,108 @@ def Dequantize( pil_image ):

	 return pil_image

+def GenerateNumPyImage( path, mime, force_pil = False ):
+
+	if HG.media_load_report_mode:
+
+		lib.Hydrus.Data.ShowText( 'Loading media: ' + path )
+
+
+	if not OPENCV_OK:
+
+		force_pil = True
+
+
+	if mime == HC.IMAGE_GIF or force_pil:
+
+		if HG.media_load_report_mode:
+
+			lib.Hydrus.Data.ShowText( 'Loading with PIL' )
+
+
+		# a regular cv.imread call, can crash the whole process on random thumbs, hooray, so have this as backup
+		# it was just the read that was the problem, so this seems to work fine, even if pil is only about half as fast
+
+		pil_image = GeneratePILImage( path )
+
+		numpy_image = GenerateNumPyImageFromPILImage( pil_image )
+
+	else:
+
+		if HG.media_load_report_mode:
+
+			lib.Hydrus.Data.ShowText( 'Loading with OpenCV' )
+
+
+		if mime == HC.IMAGE_JPEG:
+
+			flags = CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION
+
+		else:
+
+			flags = CV_IMREAD_FLAGS_SUPPORTS_ALPHA
+
+
+		numpy_image = cv2.imread( path, flags = flags )
+
+		if numpy_image is None: # doesn't support static gifs and some random other stuff
+
+			if HG.media_load_report_mode:
+
+				lib.Hydrus.Data.ShowText( 'OpenCV Failed, loading with PIL' )
+
+
+			pil_image = GeneratePILImage( path )
+
+			numpy_image = GenerateNumPyImageFromPILImage( pil_image )
+
+		else:
+
+			if numpy_image.dtype == 'uint16':
+
+				numpy_image //= 256
+
+				numpy_image = numpy.array( numpy_image, dtype = 'uint8' )
+
+
+			shape = numpy_image.shape
+
+			if len( shape ) == 2:
+
+				# monochrome image
+
+				convert = cv2.COLOR_GRAY2RGB
+
+			else:
+
+				( im_y, im_x, depth ) = shape
+
+				if depth == 4:
+
+					convert = cv2.COLOR_BGRA2RGBA
+
+				else:
+
+					convert = cv2.COLOR_BGR2RGB
+
+
+
+			numpy_image = cv2.cvtColor( numpy_image, convert )
+
+
+
+	return numpy_image
+
+def GenerateNumPyImageFromPILImage( pil_image ):
+
+	pil_image = Dequantize( pil_image )
+
+	( w, h ) = pil_image.size
+
+	s = pil_image.tobytes()
+
+	return numpy.fromstring( s, dtype = 'uint8' ).reshape( ( h, w, len( s ) // ( w * h ) ) )
+
 def GeneratePILImage( path ):

	 fp = open( path, 'rb' )
@@ -91,13 +227,13 @@ def GeneratePILImage( path ):

		 pil_image = PILImage.open( fp )

-	except Exception:
+	except Exception as e:

		 # pil doesn't clean up its open file on exception, jej

		 fp.close()

-		raise
+		raise lib.Hydrus.Exceptions.MimeException( 'Could not load the image!' + os.linesep + traceback.format_exc() )

	 if pil_image.format == 'JPEG' and hasattr( pil_image, '_getexif' ):
@@ -178,7 +314,7 @@ def GeneratePILImage( path ):

	 return pil_image

-def GeneratePILImageFromNumpyImage( numpy_image ):
+def GeneratePILImageFromNumPyImage( numpy_image ):

	 ( h, w, depth ) = numpy_image.shape

@@ -195,6 +331,107 @@ def GeneratePILImageFromNumpyImage( numpy_image ):

	 return pil_image

+def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime ):
+
+	if OPENCV_OK and mime != HC.IMAGE_GIF:
+
+		numpy_image = GenerateNumPyImage( path, mime )
+
+		thumbnail_numpy_image = ResizeNumPyImage( numpy_image, target_resolution )
+
+		try:
+
+			thumbnail_bytes = GenerateThumbnailBytesNumPy( thumbnail_numpy_image, mime )
+
+			return thumbnail_bytes
+
+		except lib.Hydrus.Exceptions.CantRenderWithCVException:
+
+			pass # fall back to pil
+
+
+
+	pil_image = GeneratePILImage( path )
+
+	pil_image = Dequantize( pil_image )
+
+	thumbnail_pil_image = pil_image.resize( target_resolution, PILImage.ANTIALIAS )
+
+	thumbnail_bytes = GenerateThumbnailBytesPIL( pil_image, mime )
+
+	return thumbnail_bytes
+
+def GenerateThumbnailBytesNumPy( numpy_image, mime ):
+
+	if not OPENCV_OK:
+
+		pil_image = GeneratePILImageFromNumPyImage( numpy_image )
+
+		return GenerateThumbnailBytesPIL( pil_image, mime )
+
+
+	( im_y, im_x, depth ) = numpy_image.shape
+
+	if depth == 4:
+
+		convert = cv2.COLOR_RGBA2BGRA
+
+	else:
+
+		convert = cv2.COLOR_RGB2BGR
+
+
+	numpy_image = cv2.cvtColor( numpy_image, convert )
+
+	if mime == HC.IMAGE_JPEG:
+
+		ext = '.jpg'
+
+		params = CV_JPEG_THUMBNAIL_ENCODE_PARAMS
+
+	else:
+
+		ext = '.png'
+
+		params = CV_PNG_THUMBNAIL_ENCODE_PARAMS
+
+
+	( result_success, result_byte_array ) = cv2.imencode( ext, numpy_image, params )
+
+	if result_success:
+
+		thumbnail_bytes = result_byte_array.tostring()
+
+		return thumbnail_bytes
+
+	else:
+
+		raise lib.Hydrus.Exceptions.CantRenderWithCVException( 'Thumb failed to encode!' )
+
+
+def GenerateThumbnailBytesPIL( pil_image, mime ):
+
+	f = io.BytesIO()
+
+	pil_image = Dequantize( pil_image )
+
+	if mime == HC.IMAGE_PNG or pil_image.mode == 'RGBA':
+
+		pil_image.save( f, 'PNG' )
+
+	else:
+
+		pil_image.save( f, 'JPEG', quality = 92 )
+
+
+	f.seek( 0 )
+
+	thumbnail_bytes = f.read()
+
+	f.close()
+
+	return thumbnail_bytes
+
 def GetGIFFrameDurations( path ):

	 pil_image = GeneratePILImage( path )
@@ -238,19 +475,31 @@ def GetGIFFrameDurations( path ):

 def GetImageProperties( path, mime ):

-	( ( width, height ), num_frames ) = GetResolutionAndNumFrames( path, mime )
-
-	if num_frames > 1:
-
-		durations = GetGIFFrameDurations( path )
+	if OPENCV_OK and mime != HC.IMAGE_GIF: # webp here too maybe eventually, or offload it all to ffmpeg

-		duration = sum( durations )
+		numpy_image = GenerateNumPyImage( path, mime )

-	else:
+		( width, height ) = GetResolutionNumPy( numpy_image )

		 duration = None
		 num_frames = None

+	else:
+
+		( ( width, height ), num_frames ) = GetResolutionAndNumFramesPIL( path, mime )
+
+		if num_frames > 1:
+
+			durations = GetGIFFrameDurations( path )
+
+			duration = sum( durations )
+
+		else:
+
+			duration = None
+			num_frames = None
+
+

	 return ( ( width, height ), duration, num_frames )

@@ -269,7 +518,13 @@ def GetPSDResolution( path ):

	 return ( width, height )

-def GetResolutionAndNumFrames( path, mime ):
+def GetResolutionNumPy( numpy_image ):
+
+	( image_height, image_width, depth ) = numpy_image.shape
+
+	return ( image_width, image_height )
+
+def GetResolutionAndNumFramesPIL( path, mime ):

	 pil_image = GeneratePILImage( path )

@@ -306,10 +561,10 @@ def GetResolutionAndNumFrames( path, mime ):

	 return ( ( x, y ), num_frames )

-def GetThumbnailResolution( image_resolution, bounding_resolution ):
+def GetThumbnailResolution( image_resolution, bounding_dimensions ):

	 ( im_width, im_height ) = image_resolution
-	( bounding_width, bounding_height ) = bounding_resolution
+	( bounding_width, bounding_height ) = bounding_dimensions

	 if bounding_width >= im_width and bounding_height >= im_height:

@@ -360,10 +615,23 @@ def IsDecompressionBomb( path ):

	 return False

-def ResizePILImage( pil_image, target_resolution ):
+def ResizeNumPyImage( numpy_image, target_resolution ):
+
+	( target_width, target_height ) = target_resolution
+	( image_width, image_height ) = GetResolutionNumPy( numpy_image )

-	( target_x, target_y ) = target_resolution
-	( im_x, im_y ) = pil_image.size
+	if target_width == image_width and target_height == target_width:
+
+		return numpy_image
+
+	elif target_width > image_height or target_height > image_width:
+
+		interpolation = cv2.INTER_LANCZOS4
+
+	else:
+
+		interpolation = cv2.INTER_AREA
+

-	return pil_image.resize( ( target_x, target_y ), PILImage.ANTIALIAS )
+	return cv2.resize( numpy_image, ( target_width, target_height ), interpolation = interpolation )

diff --git a/include/lib.Hydrus.Paths.py b/include/lib.Hydrus.Paths.py
index c91f3f1..e6fde39 100644
--- a/include/lib.Hydrus.Paths.py
+++ b/include/lib.Hydrus.Paths.py
@@ -508,10 +508,7 @@ def LaunchFile( path, launch_path = None ):

 def MakeSureDirectoryExists( path ):

-	if not os.path.exists( path ):
-
-		os.makedirs( path )
-
+	os.makedirs( path, exist_ok = True )

 def MakeFileWritable( path, recursive = True ):

diff --git a/include/lib.Hydrus.Serialisable.py b/include/lib.Hydrus.Serialisable.py
index 477012c..2b5bbf5 100644
--- a/include/lib.Hydrus.Serialisable.py
+++ b/include/lib.Hydrus.Serialisable.py
@@ -64,7 +64,7 @@ HG.SerialisableType["NETWORK_SESSION_MANAGER"] = 46
 HG.SerialisableType["NETWORK_CONTEXT"] = 47
 HG.SerialisableType["NETWORK_LOGIN_MANAGER"] = 48
 HG.SerialisableType["MEDIA_SORT"] = 49
-HG.SerialisableType["URL_MATCH"] = 50
+HG.SerialisableType["URL_CLASS"] = 50
 HG.SerialisableType["STRING_MATCH"] = 51
 HG.SerialisableType["CHECKER_OPTIONS"] = 52
 HG.SerialisableType["NETWORK_DOMAIN_MANAGER"] = 53
diff --git a/include/lib.Hydrus.Server.Resources.py b/include/lib.Hydrus.Server.Resources.py
index b8056d9..186bdc3 100644
--- a/include/lib.Hydrus.Server.Resources.py
+++ b/include/lib.Hydrus.Server.Resources.py
@@ -253,7 +253,9 @@ def ParseFileArguments( path, decompression_bombs_ok = False ):

			 bounding_dimensions = HC.SERVER_THUMBNAIL_DIMENSIONS

-			thumbnail_bytes = lib.Handling.GenerateThumbnailBytes( path, bounding_dimensions, mime, width, height, duration, num_frames )
+			target_resolution = lib.Handling.Image.GetThumbnailResolution( ( width, height ), bounding_dimensions )
+
+			thumbnail_bytes = lib.Handling.GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames )

		 except Exception as e:

diff --git a/include/lib.Hydrus.DB.Remote.py b/include/lib.Hydrus.DB.Remote.py
index 32a1f54..ca7a805 100755
--- a/include/lib.Hydrus.DB.Remote.py
+++ b/include/lib.Hydrus.DB.Remote.py
@@ -782,7 +782,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

	 def _GetHashes( self, master_hash_ids ):

-		select_statement = 'SELECT hash FROM hashes WHERE master_hash_id IN %s;'
+		select_statement = 'SELECT hash FROM hashes WHERE master_hash_id IN {};'

		 return [ hash for ( hash, ) in self._SelectFromList( select_statement, master_hash_ids ) ]

@@ -1319,7 +1319,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 else:

-			select_statement = 'SELECT service_hash_id FROM ' + deleted_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id IN %s;'
+			select_statement = 'SELECT service_hash_id FROM ' + deleted_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id IN {};'

			 deleted_service_hash_ids = self._STI( self._SelectFromList( select_statement, service_hash_ids ) )

@@ -1569,7 +1569,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name, ip_addresses_table_name ) = GenerateRepositoryFilesTableNames( service_id )

-		select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE service_hash_id IN %s;'
+		select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE service_hash_id IN {};'

		 valid_service_hash_ids = self._STL( self._SelectFromList( select_statement, service_hash_ids ) )

@@ -1587,7 +1587,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateRepositoryMappingsTableNames( service_id )

-		select_statement = 'SELECT service_hash_id FROM ' + current_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id IN %s;'
+		select_statement = 'SELECT service_hash_id FROM ' + current_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id IN {};'

		 valid_service_hash_ids = self._STL( self._SelectFromList( select_statement, service_hash_ids ) )

@@ -2146,7 +2146,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( hash_id_map_table_name, tag_id_map_table_name ) = GenerateRepositoryMasterMapTableNames( service_id )

-		select_statement = 'SELECT master_hash_id FROM ' + hash_id_map_table_name + ' WHERE service_hash_id IN %s;'
+		select_statement = 'SELECT master_hash_id FROM ' + hash_id_map_table_name + ' WHERE service_hash_id IN {};'

		 master_hash_ids = [ master_hash_id for ( master_hash_id, ) in self._SelectFromList( select_statement, service_hash_ids ) ]

@@ -2527,7 +2527,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name, ip_addresses_table_name ) = GenerateRepositoryFilesTableNames( service_id )

-		select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE service_hash_id IN %s;'
+		select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE service_hash_id IN {};'

		 valid_service_hash_ids = [ service_hash_id for ( service_hash_id, ) in self._SelectFromList( select_statement, service_hash_ids ) ]

@@ -2540,7 +2540,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateRepositoryMappingsTableNames( service_id )

-		select_statement = 'SELECT service_hash_id FROM ' + current_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id IN %s;'
+		select_statement = 'SELECT service_hash_id FROM ' + current_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id IN {};'

		 valid_service_hash_ids = [ service_hash_id for ( service_hash_id, ) in self._SelectFromList( select_statement, service_hash_ids ) ]

@@ -2884,7 +2884,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name, ip_addresses_table_name ) = GenerateRepositoryFilesTableNames( service_id )

-		select_statement = 'SELECT account_id, COUNT( * ) FROM ' + petitioned_files_table_name + ' WHERE service_hash_id IN %s GROUP BY account_id;'
+		select_statement = 'SELECT account_id, COUNT( * ) FROM ' + petitioned_files_table_name + ' WHERE service_hash_id IN {} GROUP BY account_id;'

		 scores = [ ( account_id, count * multiplier ) for ( account_id, count ) in self._SelectFromList( select_statement, service_hash_ids ) ]

@@ -2895,7 +2895,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateRepositoryMappingsTableNames( service_id )

-		select_statement = 'SELECT account_id, COUNT( * ) FROM ' + petitioned_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id IN %s GROUP BY account_id;'
+		select_statement = 'SELECT account_id, COUNT( * ) FROM ' + petitioned_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id IN {} GROUP BY account_id;'

		 scores = [ ( account_id, count * multiplier ) for ( account_id, count ) in self._SelectFromList( select_statement, service_hash_ids ) ]

@@ -3010,7 +3010,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name, ip_addresses_table_name ) = GenerateRepositoryFilesTableNames( service_id )

-		select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE account_id IN %s;'
+		select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE account_id IN {};'

		 service_hash_ids = [ service_hash_id for ( service_hash_id, ) in self._SelectFromList( select_statement, subject_account_ids ) ]

@@ -3021,7 +3021,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateRepositoryMappingsTableNames( service_id )

-		select_statement = 'SELECT service_tag_id, service_hash_id FROM ' + current_mappings_table_name + ' WHERE account_id IN %s;'
+		select_statement = 'SELECT service_tag_id, service_hash_id FROM ' + current_mappings_table_name + ' WHERE account_id IN {};'

		 mappings_dict = lib.Hydrus.Data.BuildKeyToListDict( self._SelectFromList( select_statement, subject_account_ids ) )

@@ -3035,7 +3035,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_tag_parents_table_name, deleted_tag_parents_table_name, pending_tag_parents_table_name, petitioned_tag_parents_table_name ) = GenerateRepositoryTagParentsTableNames( service_id )

-		select_statement = 'SELECT child_service_tag_id, parent_service_tag_id FROM ' + current_tag_parents_table_name + ' WHERE account_id IN %s;'
+		select_statement = 'SELECT child_service_tag_id, parent_service_tag_id FROM ' + current_tag_parents_table_name + ' WHERE account_id IN {};'

		 pairs = self._SelectFromListFetchAll( select_statement, subject_account_ids )

@@ -3049,7 +3049,7 @@ class DB( lib.Hydrus.DB.HydrusDB ):

		 ( current_tag_siblings_table_name, deleted_tag_siblings_table_name, pending_tag_siblings_table_name, petitioned_tag_siblings_table_name ) = GenerateRepositoryTagSiblingsTableNames( service_id )

-		select_statement = 'SELECT bad_service_tag_id, good_service_tag_id FROM ' + current_tag_siblings_table_name + ' WHERE account_id IN %s;'
+		select_statement = 'SELECT bad_service_tag_id, good_service_tag_id FROM ' + current_tag_siblings_table_name + ' WHERE account_id IN {};'

		 pairs = self._SelectFromListFetchAll( select_statement, subject_account_ids )

diff --git a/include/Testlib.Handling.Image.Local.py b/include/Testlib.Handling.Image.Local.py
index ec32067..14cd49a 100644
--- a/include/Testlib.Handling.Image.Local.py
+++ b/include/Testlib.Handling.Image.Local.py
@@ -1,3 +1,4 @@
+import lib.Hydrus.Constants.Local as CC
 import lib.Handling.Image.Local
 import collections
 import lib.Hydrus.Constants as HC
@@ -12,3 +13,11 @@ class TestImageHandling( unittest.TestCase ):

		 self.assertEqual( phashes, set( [ b'\xb4M\xc7\xb2M\xcb8\x1c' ] ) )

+		phashes = lib.Handling.Image.Local.DiscardBlankPerceptualHashes( { CC.BLANK_PHASH } )
+
+		self.assertEqual( phashes, set() )
+
+		phashes = lib.Handling.Image.Local.DiscardBlankPerceptualHashes( { b'\xb4M\xc7\xb2M\xcb8\x1c', CC.BLANK_PHASH } )
+
+		self.assertEqual( phashes, set( [ b'\xb4M\xc7\xb2M\xcb8\x1c' ] ) )
+