1620 lines
54 KiB
Python
1620 lines
54 KiB
Python
import collections
|
|
import itertools
|
|
import threading
|
|
import typing
|
|
|
|
from hydrus.core import HydrusConstants as HC
|
|
from hydrus.core import HydrusTags
|
|
from hydrus.core import HydrusData
|
|
from hydrus.core import HydrusExceptions
|
|
from hydrus.core import HydrusGlobals as HG
|
|
|
|
from hydrus.client import ClientConstants as CC
|
|
from hydrus.client import ClientLocation
|
|
from hydrus.client import ClientSearch
|
|
from hydrus.client import ClientTime
|
|
from hydrus.client.metadata import ClientTags
|
|
|
|
class DuplicatesManager( object ):
|
|
|
|
def __init__( self, service_keys_to_dupe_statuses_to_counts ):
|
|
|
|
self._service_keys_to_dupe_statuses_to_counts = service_keys_to_dupe_statuses_to_counts
|
|
|
|
|
|
def Duplicate( self ):
|
|
|
|
service_keys_to_dupe_statuses_to_counts = collections.defaultdict( collections.Counter )
|
|
|
|
return DuplicatesManager( service_keys_to_dupe_statuses_to_counts )
|
|
|
|
|
|
def GetDupeStatusesToCounts( self, service_key ):
|
|
|
|
return self._service_keys_to_dupe_statuses_to_counts[ service_key ]
|
|
|
|
|
|
class FileInfoManager( object ):
|
|
|
|
def __init__(
|
|
self,
|
|
hash_id: int,
|
|
hash: bytes,
|
|
size: typing.Optional[ int ] = None,
|
|
mime: typing.Optional[ int ] = None,
|
|
width: typing.Optional[ int ] = None,
|
|
height: typing.Optional[ int ] = None,
|
|
duration: typing.Optional[ int ] = None,
|
|
num_frames: typing.Optional[ int ] = None,
|
|
has_audio: typing.Optional[ bool ] = None,
|
|
num_words: typing.Optional[ int ] = None
|
|
):
|
|
|
|
if mime is None:
|
|
|
|
mime = HC.APPLICATION_UNKNOWN
|
|
|
|
|
|
if mime in HC.MIMES_WITH_THUMBNAILS:
|
|
|
|
if width is None or width <= 0:
|
|
|
|
width = 1
|
|
|
|
|
|
if height is None or height <= 0:
|
|
|
|
height = 1
|
|
|
|
|
|
self.hash_id = hash_id
|
|
self.hash = hash
|
|
self.size = size
|
|
self.mime = mime
|
|
self.width = width
|
|
self.height = height
|
|
self.duration = duration
|
|
self.num_frames = num_frames
|
|
self.has_audio = has_audio
|
|
self.num_words = num_words
|
|
|
|
|
|
def Duplicate( self ):
|
|
|
|
return FileInfoManager( self.hash_id, self.hash, self.size, self.mime, self.width, self.height, self.duration, self.num_frames, self.has_audio, self.num_words )
|
|
|
|
|
|
def ToTuple( self ):
|
|
|
|
return ( self.hash_id, self.hash, self.size, self.mime, self.width, self.height, self.duration, self.num_frames, self.has_audio, self.num_words )
|
|
|
|
|
|
class FileViewingStatsManager( object ):
|
|
|
|
def __init__(
|
|
self,
|
|
view_rows: typing.Collection
|
|
):
|
|
|
|
self.last_viewed_timestamps = {}
|
|
self.views = collections.Counter()
|
|
self.viewtimes = collections.Counter()
|
|
|
|
for ( canvas_type, last_viewed_timestamp, views, viewtime ) in view_rows:
|
|
|
|
if last_viewed_timestamp is not None:
|
|
|
|
self.last_viewed_timestamps[ canvas_type ] = last_viewed_timestamp
|
|
|
|
|
|
if views != 0:
|
|
|
|
self.views[ canvas_type ] = views
|
|
|
|
|
|
if viewtime != 0:
|
|
|
|
self.viewtimes[ canvas_type ] = viewtime
|
|
|
|
|
|
|
|
|
|
def Duplicate( self ) -> "FileViewingStatsManager":
|
|
|
|
view_rows = []
|
|
|
|
for canvas_type in ( CC.CANVAS_MEDIA_VIEWER, CC.CANVAS_PREVIEW ):
|
|
|
|
if canvas_type in self.last_viewed_timestamps:
|
|
|
|
last_viewed_timestamp = self.last_viewed_timestamps[ canvas_type ]
|
|
|
|
else:
|
|
|
|
last_viewed_timestamp = None
|
|
|
|
|
|
views = self.views[ canvas_type ]
|
|
viewtime = self.viewtimes[ canvas_type ]
|
|
|
|
view_rows.append( ( canvas_type, last_viewed_timestamp, views, viewtime ) )
|
|
|
|
|
|
return FileViewingStatsManager( view_rows )
|
|
|
|
|
|
def GetLastViewedTime( self, canvas_type: int ) -> typing.Optional[ int ]:
|
|
|
|
if canvas_type in self.last_viewed_timestamps:
|
|
|
|
return self.last_viewed_timestamps[ canvas_type ]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def GetPrettyViewsLine( self, canvas_types: typing.Collection[ int ] ) -> str:
|
|
|
|
if len( canvas_types ) == 2:
|
|
|
|
info_string = ''
|
|
|
|
elif CC.CANVAS_MEDIA_VIEWER in canvas_types:
|
|
|
|
info_string = ' in media viewer'
|
|
|
|
elif CC.CANVAS_PREVIEW in canvas_types:
|
|
|
|
info_string = ' in preview window'
|
|
|
|
|
|
views_total = sum( ( self.views[ canvas_type ] for canvas_type in canvas_types ) )
|
|
viewtime_total = sum( ( self.viewtimes[ canvas_type ] for canvas_type in canvas_types ) )
|
|
|
|
if views_total == 0:
|
|
|
|
return 'no view record{}'.format( info_string )
|
|
|
|
|
|
last_viewed_times = []
|
|
|
|
for canvas_type in canvas_types:
|
|
|
|
if canvas_type in self.last_viewed_timestamps:
|
|
|
|
last_viewed_times.append( self.last_viewed_timestamps[ canvas_type ] )
|
|
|
|
|
|
|
|
if len( last_viewed_times ) == 0:
|
|
|
|
last_viewed_string = 'no recorded last view time'
|
|
|
|
else:
|
|
|
|
last_viewed_string = 'last {}'.format( HydrusData.TimestampToPrettyTimeDelta( max( last_viewed_times ) ) )
|
|
|
|
|
|
return 'viewed {} times{}, totalling {}, {}'.format( HydrusData.ToHumanInt( views_total ), info_string, HydrusData.TimeDeltaToPrettyTimeDelta( viewtime_total ), last_viewed_string )
|
|
|
|
|
|
def GetViews( self, canvas_type: int ) -> int:
|
|
|
|
return self.views[ canvas_type ]
|
|
|
|
|
|
def GetViewtime( self, canvas_type: int ) -> int:
|
|
|
|
return self.viewtimes[ canvas_type ]
|
|
|
|
|
|
def MergeCounts( self, file_viewing_stats_manager: "FileViewingStatsManager" ):
|
|
|
|
for ( canvas_type, last_viewed_timestamp ) in file_viewing_stats_manager.last_viewed_timestamps.items():
|
|
|
|
if canvas_type in self.last_viewed_timestamps:
|
|
|
|
self.last_viewed_timestamps[ canvas_type ] = max( self.last_viewed_timestamps[ canvas_type ], last_viewed_timestamp )
|
|
|
|
else:
|
|
|
|
self.last_viewed_timestamps[ canvas_type ] = last_viewed_timestamp
|
|
|
|
|
|
|
|
self.views.update( file_viewing_stats_manager.views )
|
|
self.viewtimes.update( file_viewing_stats_manager.viewtimes )
|
|
|
|
|
|
def ProcessContentUpdate( self, content_update ):
|
|
|
|
( data_type, action, row ) = content_update.ToTuple()
|
|
|
|
if action == HC.CONTENT_UPDATE_ADD:
|
|
|
|
( hash, canvas_type, view_timestamp, views_delta, viewtime_delta ) = row
|
|
|
|
if view_timestamp is not None:
|
|
|
|
self.last_viewed_timestamps[ canvas_type ] = view_timestamp
|
|
|
|
|
|
self.views[ canvas_type ] += views_delta
|
|
self.viewtimes[ canvas_type ] += viewtime_delta
|
|
|
|
elif action == HC.CONTENT_UPDATE_DELETE:
|
|
|
|
self.last_viewed_timestamps = {}
|
|
self.views = collections.Counter()
|
|
self.viewtimes = collections.Counter()
|
|
|
|
|
|
|
|
@staticmethod
|
|
def STATICGenerateCombinedManager( sub_fvsms: typing.Iterable[ "FileViewingStatsManager" ] ):
|
|
|
|
fvsm = FileViewingStatsManager.STATICGenerateEmptyManager()
|
|
|
|
for sub_fvsm in sub_fvsms:
|
|
|
|
fvsm.MergeCounts( sub_fvsm )
|
|
|
|
|
|
return fvsm
|
|
|
|
|
|
@staticmethod
|
|
def STATICGenerateEmptyManager():
|
|
|
|
return FileViewingStatsManager( [] )
|
|
|
|
|
|
class TimestampManager( object ):
|
|
|
|
DOMAIN_FILE_MODIFIED = 0
|
|
DOMAIN_ARCHIVED = 1
|
|
DOMAIN_AGGREGATE_MODIFIED = 2
|
|
|
|
def __init__( self ):
|
|
|
|
self._domains_to_timestamps = dict()
|
|
|
|
self._aggregate_modified_is_generated = False
|
|
|
|
|
|
def _ClearAggregateModifiedTimestamp( self ):
|
|
|
|
if self.DOMAIN_AGGREGATE_MODIFIED in self._domains_to_timestamps:
|
|
|
|
del self._domains_to_timestamps[ self.DOMAIN_AGGREGATE_MODIFIED ]
|
|
|
|
|
|
self._aggregate_modified_is_generated = False
|
|
|
|
|
|
def _GenerateAggregateModifiedTimestamp( self ):
|
|
|
|
all_timestamps = [ value for ( key, value ) in self._domains_to_timestamps.items() if isinstance( key, str ) or key == self.DOMAIN_FILE_MODIFIED ]
|
|
|
|
if len( all_timestamps ) > 0:
|
|
|
|
self._domains_to_timestamps[ self.DOMAIN_AGGREGATE_MODIFIED ] = min( all_timestamps )
|
|
|
|
|
|
# we can complete this task and not populate the dict, so we'll handle 'did we do it?' checks with a bool, otherwise we'll loop the non-generation over and over
|
|
self._aggregate_modified_is_generated = True
|
|
|
|
|
|
def _GetTimestamp( self, domain ):
|
|
|
|
if domain in self._domains_to_timestamps:
|
|
|
|
return self._domains_to_timestamps[ domain ]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def ClearArchivedTimestamp( self ):
|
|
|
|
if self.DOMAIN_ARCHIVED in self._domains_to_timestamps:
|
|
|
|
del self._domains_to_timestamps[ self.DOMAIN_ARCHIVED ]
|
|
|
|
|
|
|
|
def ClearDomainModifiedTimestamp( self, domain: str ):
|
|
|
|
if domain in self._domains_to_timestamps:
|
|
|
|
del self._domains_to_timestamps[ domain ]
|
|
|
|
self._ClearAggregateModifiedTimestamp()
|
|
|
|
|
|
|
|
def Duplicate( self ) -> "TimestampManager":
|
|
|
|
timestamp_manager = TimestampManager()
|
|
|
|
timestamp_manager._domains_to_timestamps = dict( self._domains_to_timestamps )
|
|
|
|
return timestamp_manager
|
|
|
|
|
|
def GetAggregateModifiedTimestamp( self ):
|
|
|
|
if not self._aggregate_modified_is_generated:
|
|
|
|
self._GenerateAggregateModifiedTimestamp()
|
|
|
|
|
|
return self._GetTimestamp( self.DOMAIN_AGGREGATE_MODIFIED )
|
|
|
|
|
|
def GetArchivedTimestamp( self ) -> typing.Optional[ int ]:
|
|
|
|
return self._GetTimestamp( self.DOMAIN_ARCHIVED )
|
|
|
|
|
|
def GetDomainModifiedTimestamp( self, domain: str ) -> typing.Optional[ int ]:
|
|
|
|
return self._GetTimestamp( domain )
|
|
|
|
|
|
def GetFileModifiedTimestamp( self ) -> typing.Optional[ int ]:
|
|
|
|
return self._GetTimestamp( self.DOMAIN_FILE_MODIFIED )
|
|
|
|
|
|
def SetArchivedTimestamp( self, timestamp: int ):
|
|
|
|
self._domains_to_timestamps[ self.DOMAIN_ARCHIVED ] = timestamp
|
|
|
|
|
|
def SetDomainModifiedTimestamp( self, domain: str, timestamp: int ):
|
|
|
|
self._domains_to_timestamps[ domain ] = timestamp
|
|
|
|
self._ClearAggregateModifiedTimestamp()
|
|
|
|
|
|
def SetFileModifiedTimestamp( self, timestamp: int ):
|
|
|
|
self._domains_to_timestamps[ self.DOMAIN_FILE_MODIFIED ] = timestamp
|
|
|
|
self._ClearAggregateModifiedTimestamp()
|
|
|
|
|
|
def UpdateDomainModifiedTimestamp( self, domain: str, timestamp: int ):
|
|
|
|
existing_timestamp = self._GetTimestamp( domain )
|
|
|
|
if existing_timestamp is None or ClientTime.ShouldUpdateDomainModifiedTime( existing_timestamp, timestamp ):
|
|
|
|
self.SetDomainModifiedTimestamp( domain, timestamp )
|
|
|
|
|
|
|
|
class LocationsManager( object ):
|
|
|
|
def __init__(
|
|
self,
|
|
current_to_timestamps: typing.Dict[ bytes, typing.Optional[ int ] ],
|
|
deleted_to_timestamps: typing.Dict[ bytes, typing.Tuple[ int, int ] ],
|
|
pending: typing.Set[ bytes ],
|
|
petitioned: typing.Set[ bytes ],
|
|
inbox: bool = False,
|
|
urls: typing.Optional[ typing.Set[ str ] ] = None,
|
|
service_keys_to_filenames: typing.Optional[ typing.Dict[ bytes, str ] ] = None,
|
|
timestamp_manager: typing.Optional[ TimestampManager ] = None,
|
|
local_file_deletion_reason: str = None
|
|
):
|
|
|
|
self._current_to_timestamps = current_to_timestamps
|
|
self._deleted_to_timestamps = deleted_to_timestamps
|
|
|
|
self._current = set( self._current_to_timestamps.keys() )
|
|
self._deleted = set( self._deleted_to_timestamps.keys() )
|
|
self._pending = pending
|
|
self._petitioned = petitioned
|
|
|
|
self.inbox = inbox
|
|
|
|
if urls is None:
|
|
|
|
urls = set()
|
|
|
|
|
|
self._urls = urls
|
|
|
|
if service_keys_to_filenames is None:
|
|
|
|
service_keys_to_filenames = {}
|
|
|
|
|
|
self._service_keys_to_filenames = service_keys_to_filenames
|
|
|
|
if timestamp_manager is None:
|
|
|
|
timestamp_manager = TimestampManager()
|
|
|
|
|
|
self._timestamp_manager = timestamp_manager
|
|
self._local_file_deletion_reason = local_file_deletion_reason
|
|
|
|
|
|
def DeletePending( self, service_key ):
|
|
|
|
self._pending.discard( service_key )
|
|
self._petitioned.discard( service_key )
|
|
|
|
|
|
def Duplicate( self ):
|
|
|
|
current_to_timestamps = dict( self._current_to_timestamps )
|
|
deleted_to_timestamps = dict( self._deleted_to_timestamps )
|
|
pending = set( self._pending )
|
|
petitioned = set( self._petitioned )
|
|
urls = set( self._urls )
|
|
service_keys_to_filenames = dict( self._service_keys_to_filenames )
|
|
timestamp_manager = self._timestamp_manager.Duplicate()
|
|
|
|
return LocationsManager(
|
|
current_to_timestamps,
|
|
deleted_to_timestamps,
|
|
pending,
|
|
petitioned,
|
|
inbox = self.inbox,
|
|
urls = urls,
|
|
service_keys_to_filenames = service_keys_to_filenames,
|
|
timestamp_manager = timestamp_manager,
|
|
local_file_deletion_reason = self._local_file_deletion_reason
|
|
)
|
|
|
|
|
|
def GetCDPP( self ):
|
|
|
|
return ( self._current, self._deleted, self._pending, self._petitioned )
|
|
|
|
|
|
def GetCurrent( self ):
|
|
|
|
return self._current
|
|
|
|
|
|
def GetDeleted( self ):
|
|
|
|
return self._deleted
|
|
|
|
|
|
def GetInbox( self ):
|
|
|
|
return self.inbox
|
|
|
|
|
|
def GetPending( self ):
|
|
|
|
return self._pending
|
|
|
|
|
|
def GetPetitioned( self ):
|
|
|
|
return self._petitioned
|
|
|
|
|
|
def GetRemoteLocationStrings( self ):
|
|
|
|
remote_file_services = list( HG.client_controller.services_manager.GetServices( ( HC.FILE_REPOSITORY, HC.IPFS ) ) )
|
|
|
|
remote_file_services.sort( key = lambda s: s.GetName() )
|
|
|
|
remote_service_strings = []
|
|
|
|
for remote_service in remote_file_services:
|
|
|
|
name = remote_service.GetName()
|
|
service_key = remote_service.GetServiceKey()
|
|
|
|
if service_key in self._pending:
|
|
|
|
remote_service_strings.append( name + ' (+)' )
|
|
|
|
elif service_key in self._current:
|
|
|
|
if service_key in self._petitioned:
|
|
|
|
remote_service_strings.append( name + ' (-)' )
|
|
|
|
else:
|
|
|
|
remote_service_strings.append( name )
|
|
|
|
|
|
|
|
|
|
return remote_service_strings
|
|
|
|
|
|
def GetBestCurrentTimestamp( self, location_context: ClientLocation.LocationContext ):
|
|
|
|
timestamps = { self.GetCurrentTimestamp( service_key ) for service_key in location_context.current_service_keys }
|
|
|
|
timestamps.discard( None )
|
|
|
|
if len( timestamps ) == 0:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return min( timestamps )
|
|
|
|
|
|
|
|
def GetCurrentTimestamp( self, service_key ):
|
|
|
|
if service_key in self._current_to_timestamps:
|
|
|
|
return self._current_to_timestamps[ service_key ]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def GetDeletedTimestamps( self, service_key ):
|
|
|
|
if service_key in self._deleted_to_timestamps:
|
|
|
|
return self._deleted_to_timestamps[ service_key ]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def GetLocalFileDeletionReason( self ) -> str:
|
|
|
|
if self._local_file_deletion_reason is None:
|
|
|
|
return 'Unknown deletion reason.'
|
|
|
|
else:
|
|
|
|
return self._local_file_deletion_reason
|
|
|
|
|
|
|
|
def GetTimestampManager( self ) -> TimestampManager:
|
|
|
|
return self._timestamp_manager
|
|
|
|
|
|
def GetURLs( self ):
|
|
|
|
return self._urls
|
|
|
|
|
|
def HasLocalFileDeletionReason( self ) -> bool:
|
|
|
|
return self._local_file_deletion_reason is not None
|
|
|
|
|
|
def IsDownloading( self ):
|
|
|
|
return CC.COMBINED_LOCAL_FILE_SERVICE_KEY in self._pending
|
|
|
|
|
|
def IsLocal( self ):
|
|
|
|
return CC.COMBINED_LOCAL_FILE_SERVICE_KEY in self._current
|
|
|
|
|
|
def IsRemote( self ):
|
|
|
|
return CC.COMBINED_LOCAL_FILE_SERVICE_KEY not in self._current
|
|
|
|
|
|
def IsTrashed( self ):
|
|
|
|
return CC.TRASH_SERVICE_KEY in self._current
|
|
|
|
|
|
def _AddToService( self, service_key, do_undelete = False ):
|
|
|
|
import_time = HydrusData.GetNow()
|
|
|
|
if service_key in self._deleted_to_timestamps:
|
|
|
|
if do_undelete:
|
|
|
|
( delete_timestamp, import_time ) = self._deleted_to_timestamps[ service_key ]
|
|
|
|
|
|
del self._deleted_to_timestamps[ service_key ]
|
|
|
|
self._deleted.discard( service_key )
|
|
|
|
|
|
local_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
|
|
|
|
if service_key in local_service_keys:
|
|
|
|
if CC.TRASH_SERVICE_KEY in self._current_to_timestamps:
|
|
|
|
del self._current_to_timestamps[ CC.TRASH_SERVICE_KEY ]
|
|
|
|
self._current.discard( CC.TRASH_SERVICE_KEY )
|
|
|
|
|
|
self._AddToService( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
|
|
|
|
|
|
if service_key not in self._current_to_timestamps:
|
|
|
|
self._current_to_timestamps[ service_key ] = import_time
|
|
self._current.add( service_key )
|
|
|
|
if service_key == CC.COMBINED_LOCAL_FILE_SERVICE_KEY:
|
|
|
|
self.inbox = True
|
|
|
|
|
|
|
|
self._pending.discard( service_key )
|
|
|
|
|
|
def _DeleteFromService( self, service_key: bytes, reason: typing.Optional[ str ] ):
|
|
|
|
if service_key in self._current_to_timestamps:
|
|
|
|
current_timestamp = self._current_to_timestamps[ service_key ]
|
|
|
|
del self._current_to_timestamps[ service_key ]
|
|
|
|
self._current.discard( service_key )
|
|
|
|
else:
|
|
|
|
current_timestamp = None
|
|
|
|
|
|
if service_key != CC.TRASH_SERVICE_KEY and service_key not in self._deleted_to_timestamps:
|
|
|
|
self._deleted_to_timestamps[ service_key ] = ( HydrusData.GetNow(), current_timestamp )
|
|
self._deleted.add( service_key )
|
|
|
|
|
|
self._petitioned.discard( service_key )
|
|
|
|
local_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
|
|
|
|
if service_key in local_service_keys:
|
|
|
|
if reason is not None:
|
|
|
|
self._local_file_deletion_reason = reason
|
|
|
|
|
|
if self._current.isdisjoint( local_service_keys ):
|
|
|
|
self._AddToService( CC.TRASH_SERVICE_KEY )
|
|
|
|
|
|
elif service_key == CC.COMBINED_LOCAL_FILE_SERVICE_KEY:
|
|
|
|
for local_service_key in list( self._current.intersection( local_service_keys ) ):
|
|
|
|
self._DeleteFromService( local_service_key, reason )
|
|
|
|
|
|
if CC.TRASH_SERVICE_KEY in self._current:
|
|
|
|
self._DeleteFromService( CC.TRASH_SERVICE_KEY, reason )
|
|
|
|
|
|
self.inbox = False
|
|
|
|
|
|
|
|
def ProcessContentUpdate( self, service_key, content_update ):
|
|
|
|
( data_type, action, row ) = content_update.ToTuple()
|
|
|
|
if data_type == HC.CONTENT_TYPE_FILES:
|
|
|
|
if action == HC.CONTENT_UPDATE_ADVANCED:
|
|
|
|
( sub_action, hashes ) = row
|
|
|
|
if sub_action == 'delete_deleted':
|
|
|
|
if CC.TRASH_SERVICE_KEY not in self._current:
|
|
|
|
if service_key == CC.COMBINED_LOCAL_FILE_SERVICE_KEY:
|
|
|
|
service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, HC.COMBINED_LOCAL_FILE ) )
|
|
|
|
else:
|
|
|
|
service_keys = ( service_key, )
|
|
|
|
|
|
for service_key in service_keys:
|
|
|
|
if service_key in self._deleted_to_timestamps:
|
|
|
|
del self._deleted_to_timestamps[ service_key ]
|
|
self._deleted.discard( service_key )
|
|
|
|
|
|
|
|
|
|
|
|
elif action == HC.CONTENT_UPDATE_ARCHIVE:
|
|
|
|
if self.inbox:
|
|
|
|
self.inbox = False
|
|
|
|
self._timestamp_manager.SetArchivedTimestamp( HydrusData.GetNow() )
|
|
|
|
|
|
elif action == HC.CONTENT_UPDATE_INBOX:
|
|
|
|
self.inbox = True
|
|
|
|
self._timestamp_manager.ClearArchivedTimestamp()
|
|
|
|
elif action == HC.CONTENT_UPDATE_ADD:
|
|
|
|
self._AddToService( service_key )
|
|
|
|
elif action == HC.CONTENT_UPDATE_DELETE:
|
|
|
|
if content_update.HasReason():
|
|
|
|
reason = content_update.GetReason()
|
|
|
|
else:
|
|
|
|
reason = None
|
|
|
|
|
|
self._DeleteFromService( service_key, reason )
|
|
|
|
elif action == HC.CONTENT_UPDATE_UNDELETE:
|
|
|
|
self._AddToService( service_key, do_undelete = True )
|
|
|
|
elif action == HC.CONTENT_UPDATE_PEND:
|
|
|
|
if service_key not in self._current:
|
|
|
|
self._pending.add( service_key )
|
|
|
|
|
|
elif action == HC.CONTENT_UPDATE_PETITION:
|
|
|
|
if service_key not in self._deleted:
|
|
|
|
self._petitioned.add( service_key )
|
|
|
|
|
|
elif action == HC.CONTENT_UPDATE_RESCIND_PEND:
|
|
|
|
self._pending.discard( service_key )
|
|
|
|
elif action == HC.CONTENT_UPDATE_RESCIND_PETITION:
|
|
|
|
self._petitioned.discard( service_key )
|
|
|
|
elif action == HC.CONTENT_UPDATE_CLEAR_DELETE_RECORD:
|
|
|
|
if service_key in self._deleted_to_timestamps:
|
|
|
|
del self._deleted_to_timestamps[ service_key ]
|
|
|
|
self._deleted.discard( service_key )
|
|
|
|
|
|
|
|
elif data_type == HC.CONTENT_TYPE_URLS:
|
|
|
|
if action == HC.CONTENT_UPDATE_ADD:
|
|
|
|
( urls, hashes ) = row
|
|
|
|
self._urls.update( urls )
|
|
|
|
elif action == HC.CONTENT_UPDATE_DELETE:
|
|
|
|
( urls, hashes ) = row
|
|
|
|
self._urls.difference_update( urls )
|
|
|
|
|
|
|
|
elif data_type == HC.CONTENT_TYPE_TIMESTAMP:
|
|
|
|
( timestamp_type, hash, data ) = row
|
|
|
|
if timestamp_type == 'domain':
|
|
|
|
if action == HC.CONTENT_UPDATE_ADD:
|
|
|
|
( domain, timestamp ) = data
|
|
|
|
self._timestamp_manager.UpdateDomainModifiedTimestamp( domain, timestamp )
|
|
|
|
elif action == HC.CONTENT_UPDATE_SET:
|
|
|
|
( domain, timestamp ) = data
|
|
|
|
self._timestamp_manager.SetDomainModifiedTimestamp( domain, timestamp )
|
|
|
|
elif action == HC.CONTENT_UPDATE_DELETE:
|
|
|
|
domain = row
|
|
|
|
self._timestamp_manager.ClearDomainModifiedTimestamp( domain )
|
|
|
|
|
|
|
|
def ResetService( self, service_key ):
|
|
|
|
if service_key in self._current_to_timestamps:
|
|
|
|
del self._current_to_timestamps[ service_key ]
|
|
|
|
self._current.discard( service_key )
|
|
|
|
|
|
if service_key in self._deleted_to_timestamps:
|
|
|
|
del self._deleted_to_timestamps[ service_key ]
|
|
|
|
self._deleted.discard( service_key )
|
|
|
|
|
|
self._pending.discard( service_key )
|
|
self._petitioned.discard( service_key )
|
|
|
|
|
|
class NotesManager( object ):
|
|
|
|
def __init__( self, names_to_notes: typing.Dict[ str, str ] ):
|
|
|
|
self._names_to_notes = names_to_notes
|
|
|
|
|
|
def Duplicate( self ):
|
|
|
|
return NotesManager( dict( self._names_to_notes ) )
|
|
|
|
|
|
def GetNames( self ):
|
|
|
|
names = sorted( self._names_to_notes.keys() )
|
|
|
|
return names
|
|
|
|
|
|
def GetNamesToNotes( self ):
|
|
|
|
return dict( self._names_to_notes )
|
|
|
|
|
|
def SetNamesToNotes( self, names_to_notes: typing.Dict[ str, str ] ):
|
|
|
|
self._names_to_notes = names_to_notes
|
|
|
|
|
|
def GetNote( self, name: str ):
|
|
|
|
if name in self._names_to_notes:
|
|
|
|
return self._names_to_notes[ name ]
|
|
|
|
else:
|
|
|
|
raise HydrusExceptions.DataMissing( 'Note "{}" does not exist!'.format( name ) )
|
|
|
|
|
|
|
|
def GetNumNotes( self ):
|
|
|
|
return len( self._names_to_notes )
|
|
|
|
|
|
def HasNote( self, name: str ):
|
|
|
|
return name in self._names_to_notes
|
|
|
|
|
|
def ProcessContentUpdate( self, content_update ):
|
|
|
|
( data_type, action, row ) = content_update.ToTuple()
|
|
|
|
if action == HC.CONTENT_UPDATE_SET:
|
|
|
|
( hash, name, note ) = row
|
|
|
|
if note == '':
|
|
|
|
if name in self._names_to_notes:
|
|
|
|
del self._names_to_notes[ name ]
|
|
|
|
|
|
else:
|
|
|
|
self._names_to_notes[ name ] = note
|
|
|
|
|
|
elif action == HC.CONTENT_UPDATE_DELETE:
|
|
|
|
( hash, name ) = row
|
|
|
|
if name in self._names_to_notes:
|
|
|
|
del self._names_to_notes[ name ]
|
|
|
|
|
|
|
|
|
|
class RatingsManager( object ):
|
|
|
|
def __init__( self, service_keys_to_ratings: typing.Dict[ bytes, typing.Union[ None, float ] ] ):
|
|
|
|
self._service_keys_to_ratings = service_keys_to_ratings
|
|
|
|
|
|
def Duplicate( self ):
|
|
|
|
return RatingsManager( dict( self._service_keys_to_ratings ) )
|
|
|
|
|
|
def GetRating( self, service_key ):
|
|
|
|
if service_key in self._service_keys_to_ratings:
|
|
|
|
return self._service_keys_to_ratings[ service_key ]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def GetRatingSlice( self, service_keys ): return frozenset( { self._service_keys_to_ratings[ service_key ] for service_key in service_keys if service_key in self._service_keys_to_ratings } )
|
|
|
|
def GetServiceKeysToRatings( self ): return self._service_keys_to_ratings
|
|
|
|
def ProcessContentUpdate( self, service_key, content_update ):
|
|
|
|
( data_type, action, row ) = content_update.ToTuple()
|
|
|
|
if action == HC.CONTENT_UPDATE_ADD:
|
|
|
|
( rating, hashes ) = row
|
|
|
|
if rating is None and service_key in self._service_keys_to_ratings: del self._service_keys_to_ratings[ service_key ]
|
|
else: self._service_keys_to_ratings[ service_key ] = rating
|
|
|
|
|
|
|
|
def ResetService( self, service_key ):
|
|
|
|
if service_key in self._service_keys_to_ratings: del self._service_keys_to_ratings[ service_key ]
|
|
|
|
|
|
class TagsManager( object ):
|
|
|
|
def __init__(
|
|
self,
|
|
service_keys_to_statuses_to_storage_tags: typing.Dict[ bytes, typing.Dict[ int, typing.Set[ str ] ] ],
|
|
service_keys_to_statuses_to_display_tags: typing.Dict[ bytes, typing.Dict[ int, typing.Set[ str ] ] ]
|
|
):
|
|
|
|
self._tag_display_types_to_service_keys_to_statuses_to_tags = {
|
|
ClientTags.TAG_DISPLAY_STORAGE : service_keys_to_statuses_to_storage_tags,
|
|
ClientTags.TAG_DISPLAY_ACTUAL : service_keys_to_statuses_to_display_tags
|
|
}
|
|
|
|
self._storage_cache_is_dirty = True
|
|
self._display_cache_is_dirty = True
|
|
self._single_media_cache_is_dirty = True
|
|
self._selection_list_cache_is_dirty = True
|
|
|
|
self._lock = threading.Lock()
|
|
|
|
|
|
def _GetServiceKeysToStatusesToTags( self, tag_display_type ):
|
|
|
|
# this gets called a lot, so we are hardcoding some gubbins to avoid too many method calls
|
|
|
|
if tag_display_type == ClientTags.TAG_DISPLAY_STORAGE and self._storage_cache_is_dirty:
|
|
|
|
self._RecalcStorageCache()
|
|
|
|
if tag_display_type == ClientTags.TAG_DISPLAY_ACTUAL and self._display_cache_is_dirty:
|
|
|
|
self._RecalcDisplayCache()
|
|
|
|
elif tag_display_type == ClientTags.TAG_DISPLAY_SELECTION_LIST and self._selection_list_cache_is_dirty:
|
|
|
|
self._RecalcDisplayFilteredCache( ClientTags.TAG_DISPLAY_SELECTION_LIST )
|
|
|
|
elif tag_display_type == ClientTags.TAG_DISPLAY_SINGLE_MEDIA and self._single_media_cache_is_dirty:
|
|
|
|
self._RecalcDisplayFilteredCache( ClientTags.TAG_DISPLAY_SINGLE_MEDIA )
|
|
|
|
|
|
return self._tag_display_types_to_service_keys_to_statuses_to_tags[ tag_display_type ]
|
|
|
|
|
|
def _RecalcStorageCache( self ):
|
|
|
|
service_keys_to_statuses_to_tags = self._tag_display_types_to_service_keys_to_statuses_to_tags[ ClientTags.TAG_DISPLAY_STORAGE ]
|
|
|
|
# just combined service merge calculation
|
|
|
|
combined_statuses_to_tags = HydrusData.default_dict_set()
|
|
|
|
for ( service_key, source_statuses_to_tags ) in service_keys_to_statuses_to_tags.items():
|
|
|
|
if service_key == CC.COMBINED_TAG_SERVICE_KEY:
|
|
|
|
continue
|
|
|
|
|
|
for ( status, tags ) in source_statuses_to_tags.items():
|
|
|
|
combined_statuses_to_tags[ status ].update( tags )
|
|
|
|
|
|
|
|
service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ] = combined_statuses_to_tags
|
|
|
|
#
|
|
|
|
self._storage_cache_is_dirty = False
|
|
|
|
|
|
def _RecalcDisplayCache( self ):
|
|
|
|
if self._storage_cache_is_dirty:
|
|
|
|
self._RecalcStorageCache()
|
|
|
|
|
|
# display tags don't have petitioned or deleted, so we just copy from storage
|
|
|
|
source_service_keys_to_statuses_to_tags = self._tag_display_types_to_service_keys_to_statuses_to_tags[ ClientTags.TAG_DISPLAY_STORAGE ]
|
|
|
|
destination_service_keys_to_statuses_to_tags = self._tag_display_types_to_service_keys_to_statuses_to_tags[ ClientTags.TAG_DISPLAY_ACTUAL ]
|
|
|
|
combined_statuses_to_tags = HydrusData.default_dict_set()
|
|
|
|
for ( service_key, source_statuses_to_tags ) in source_service_keys_to_statuses_to_tags.items():
|
|
|
|
if service_key == CC.COMBINED_TAG_SERVICE_KEY:
|
|
|
|
continue
|
|
|
|
|
|
destination_statuses_to_tags = destination_service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
for status in ( HC.CONTENT_STATUS_DELETED, HC.CONTENT_STATUS_PETITIONED ):
|
|
|
|
if status in destination_statuses_to_tags:
|
|
|
|
del destination_statuses_to_tags[ status ]
|
|
|
|
|
|
if status in source_statuses_to_tags:
|
|
|
|
destination_statuses_to_tags[ status ] = set( source_statuses_to_tags[ status ] )
|
|
|
|
|
|
|
|
for ( status, tags ) in destination_statuses_to_tags.items():
|
|
|
|
combined_statuses_to_tags[ status ].update( tags )
|
|
|
|
|
|
|
|
destination_service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ] = combined_statuses_to_tags
|
|
|
|
#
|
|
|
|
self._display_cache_is_dirty = False
|
|
|
|
|
|
def _RecalcDisplayFilteredCache( self, tag_display_type ):
|
|
|
|
if self._display_cache_is_dirty:
|
|
|
|
self._RecalcDisplayCache()
|
|
|
|
|
|
# display filtering
|
|
|
|
tag_display_manager = HG.client_controller.tag_display_manager
|
|
|
|
source_service_keys_to_statuses_to_tags = self._tag_display_types_to_service_keys_to_statuses_to_tags[ ClientTags.TAG_DISPLAY_ACTUAL ]
|
|
|
|
destination_service_keys_to_statuses_to_tags = collections.defaultdict( HydrusData.default_dict_set )
|
|
|
|
combined_statuses_to_tags = HydrusData.default_dict_set()
|
|
|
|
for ( service_key, source_statuses_to_tags ) in source_service_keys_to_statuses_to_tags.items():
|
|
|
|
if service_key == CC.COMBINED_TAG_SERVICE_KEY:
|
|
|
|
continue
|
|
|
|
|
|
if tag_display_manager.FiltersTags( tag_display_type, service_key ):
|
|
|
|
destination_statuses_to_tags = HydrusData.default_dict_set()
|
|
|
|
for ( status, source_tags ) in source_statuses_to_tags.items():
|
|
|
|
dest_tags = tag_display_manager.FilterTags( tag_display_type, service_key, source_tags )
|
|
|
|
if len( source_tags ) != len( dest_tags ):
|
|
|
|
if len( dest_tags ) > 0:
|
|
|
|
destination_statuses_to_tags[ status ] = dest_tags
|
|
|
|
|
|
else:
|
|
|
|
destination_statuses_to_tags[ status ] = source_tags
|
|
|
|
|
|
|
|
else:
|
|
|
|
destination_statuses_to_tags = source_statuses_to_tags
|
|
|
|
|
|
destination_service_keys_to_statuses_to_tags[ service_key ] = destination_statuses_to_tags
|
|
|
|
for ( status, tags ) in destination_statuses_to_tags.items():
|
|
|
|
combined_statuses_to_tags[ status ].update( tags )
|
|
|
|
|
|
|
|
destination_service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ] = combined_statuses_to_tags
|
|
|
|
self._tag_display_types_to_service_keys_to_statuses_to_tags[ tag_display_type ] = destination_service_keys_to_statuses_to_tags
|
|
|
|
#
|
|
|
|
if tag_display_type == ClientTags.TAG_DISPLAY_SELECTION_LIST:
|
|
|
|
self._selection_list_cache_is_dirty = False
|
|
|
|
elif tag_display_type == ClientTags.TAG_DISPLAY_SINGLE_MEDIA:
|
|
|
|
self._single_media_cache_is_dirty = False
|
|
|
|
|
|
|
|
def _SetDirty( self ):
|
|
|
|
self._storage_cache_is_dirty = True
|
|
self._display_cache_is_dirty = True
|
|
self._single_media_cache_is_dirty = True
|
|
self._selection_list_cache_is_dirty = True
|
|
|
|
|
|
@staticmethod
|
|
def MergeTagsManagers( tags_managers ):
|
|
|
|
# we cheat here and just get display tags, since this is read only and storage exacts isn't super important
|
|
|
|
def CurrentAndPendingFilter( items ):
|
|
|
|
for ( service_key, statuses_to_tags ) in items:
|
|
|
|
filtered = { status : tags for ( status, tags ) in list(statuses_to_tags.items()) if status in ( HC.CONTENT_STATUS_CURRENT, HC.CONTENT_STATUS_PENDING ) }
|
|
|
|
yield ( service_key, filtered )
|
|
|
|
|
|
|
|
# [[( service_key, statuses_to_tags )]]
|
|
s_k_s_t_t_tupled = ( CurrentAndPendingFilter( tags_manager.GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_ACTUAL ).items() ) for tags_manager in tags_managers )
|
|
|
|
# [(service_key, statuses_to_tags)]
|
|
flattened_s_k_s_t_t = itertools.chain.from_iterable( s_k_s_t_t_tupled )
|
|
|
|
# service_key : [ statuses_to_tags ]
|
|
s_k_s_t_t_dict = HydrusData.BuildKeyToListDict( flattened_s_k_s_t_t )
|
|
|
|
# now let's merge so we have service_key : statuses_to_tags
|
|
|
|
merged_service_keys_to_statuses_to_tags = collections.defaultdict( HydrusData.default_dict_set )
|
|
|
|
for ( service_key, several_statuses_to_tags ) in list(s_k_s_t_t_dict.items()):
|
|
|
|
# [[( status, tags )]]
|
|
s_t_t_tupled = ( list(s_t_t.items()) for s_t_t in several_statuses_to_tags )
|
|
|
|
# [( status, tags )]
|
|
flattened_s_t_t = itertools.chain.from_iterable( s_t_t_tupled )
|
|
|
|
statuses_to_tags = HydrusData.default_dict_set()
|
|
|
|
for ( status, tags ) in flattened_s_t_t:
|
|
|
|
statuses_to_tags[ status ].update( tags )
|
|
|
|
|
|
merged_service_keys_to_statuses_to_tags[ service_key ] = statuses_to_tags
|
|
|
|
|
|
return TagsManager( merged_service_keys_to_statuses_to_tags, merged_service_keys_to_statuses_to_tags )
|
|
|
|
|
|
def DeletePending( self, service_key ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_STORAGE )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
if len( statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] ) + len( statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ] ) > 0:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] = set()
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ] = set()
|
|
|
|
self._SetDirty()
|
|
|
|
|
|
|
|
|
|
def Duplicate( self ):
|
|
|
|
with self._lock:
|
|
|
|
dupe_tags_manager = TagsManager( {}, {} )
|
|
|
|
dupe_tag_display_types_to_service_keys_to_statuses_to_tags = dict()
|
|
|
|
for ( tag_display_type, service_keys_to_statuses_to_tags ) in self._tag_display_types_to_service_keys_to_statuses_to_tags.items():
|
|
|
|
dupe_service_keys_to_statuses_to_tags = collections.defaultdict( HydrusData.default_dict_set )
|
|
|
|
for ( service_key, statuses_to_tags ) in service_keys_to_statuses_to_tags.items():
|
|
|
|
dupe_statuses_to_tags = HydrusData.default_dict_set()
|
|
|
|
for ( status, tags ) in statuses_to_tags.items():
|
|
|
|
dupe_statuses_to_tags[ status ] = set( tags )
|
|
|
|
|
|
dupe_service_keys_to_statuses_to_tags[ service_key ] = dupe_statuses_to_tags
|
|
|
|
|
|
dupe_tag_display_types_to_service_keys_to_statuses_to_tags[ tag_display_type ] = dupe_service_keys_to_statuses_to_tags
|
|
|
|
|
|
dupe_tags_manager._tag_display_types_to_service_keys_to_statuses_to_tags = dupe_tag_display_types_to_service_keys_to_statuses_to_tags
|
|
dupe_tags_manager._display_cache_is_dirty = self._display_cache_is_dirty
|
|
|
|
return dupe_tags_manager
|
|
|
|
|
|
|
|
def GetComparableNamespaceSlice( self, namespaces, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
combined_statuses_to_tags = service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ]
|
|
|
|
combined_current = combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]
|
|
combined_pending = combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]
|
|
|
|
combined = combined_current.union( combined_pending )
|
|
|
|
pairs = [ HydrusTags.SplitTag( tag ) for tag in combined ]
|
|
|
|
slice = []
|
|
|
|
for desired_namespace in namespaces:
|
|
|
|
subtags = sorted( ( HydrusTags.ConvertTagToSortable( subtag ) for ( namespace, subtag ) in pairs if namespace == desired_namespace ) )
|
|
|
|
slice.append( tuple( subtags ) )
|
|
|
|
|
|
return tuple( slice )
|
|
|
|
|
|
|
|
def GetCurrent( self, service_key, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
return statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]
|
|
|
|
|
|
|
|
def GetCurrentAndPending( self, service_key, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
return statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ].union( statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] )
|
|
|
|
|
|
|
|
def GetDeleted( self, service_key, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
return statuses_to_tags[ HC.CONTENT_STATUS_DELETED ]
|
|
|
|
|
|
|
|
def GetNamespaceSlice( self, namespaces, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
combined_statuses_to_tags = service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ]
|
|
|
|
combined_current = combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]
|
|
combined_pending = combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]
|
|
|
|
combined = combined_current.union( combined_pending )
|
|
|
|
slice = { tag for tag in combined if True in ( tag.startswith( namespace + ':' ) for namespace in namespaces ) }
|
|
|
|
slice = frozenset( slice )
|
|
|
|
return slice
|
|
|
|
|
|
|
|
def GetNumTags( self, tag_search_context: ClientSearch.TagSearchContext, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
num_tags = 0
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ tag_search_context.service_key ]
|
|
|
|
if tag_search_context.include_current_tags: num_tags += len( statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ] )
|
|
if tag_search_context.include_pending_tags: num_tags += len( statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] )
|
|
|
|
return num_tags
|
|
|
|
|
|
|
|
def GetPending( self, service_key, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
return statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]
|
|
|
|
|
|
|
|
def GetPetitioned( self, service_key, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
return statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ]
|
|
|
|
|
|
|
|
def GetServiceKeysToStatusesToTags( self, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
return service_keys_to_statuses_to_tags
|
|
|
|
|
|
|
|
def GetStatusesToTags( self, service_key, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
return service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
|
|
|
|
def HasTag( self, tag, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
combined_statuses_to_tags = service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ]
|
|
|
|
return tag in combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ] or tag in combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ]
|
|
|
|
|
|
|
|
def HasAnyOfTheseTags( self, tags, tag_display_type ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( tag_display_type )
|
|
|
|
combined_statuses_to_tags = service_keys_to_statuses_to_tags[ CC.COMBINED_TAG_SERVICE_KEY ]
|
|
|
|
return True in ( tag in combined_statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ] or tag in combined_statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] for tag in tags )
|
|
|
|
|
|
|
|
def NewTagDisplayRules( self ):
|
|
|
|
with self._lock:
|
|
|
|
self._SetDirty()
|
|
|
|
|
|
|
|
def ProcessContentUpdate( self, service_key, content_update: HydrusData.ContentUpdate ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_STORAGE )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
( data_type, action, row ) = content_update.ToTuple()
|
|
|
|
( tag, hashes ) = row
|
|
|
|
if action == HC.CONTENT_UPDATE_ADD:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ].add( tag )
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].discard( tag )
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].discard( tag )
|
|
|
|
elif action == HC.CONTENT_UPDATE_DELETE:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].add( tag )
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ].discard( tag )
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ].discard( tag )
|
|
|
|
elif action == HC.CONTENT_UPDATE_PEND:
|
|
|
|
if tag not in statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].add( tag )
|
|
|
|
|
|
elif action == HC.CONTENT_UPDATE_RESCIND_PEND:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].discard( tag )
|
|
|
|
elif action == HC.CONTENT_UPDATE_PETITION:
|
|
|
|
if tag in statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ].add( tag )
|
|
|
|
|
|
elif action == HC.CONTENT_UPDATE_RESCIND_PETITION:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ].discard( tag )
|
|
|
|
elif action == HC.CONTENT_UPDATE_CLEAR_DELETE_RECORD:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].discard( tag )
|
|
|
|
|
|
#
|
|
|
|
# this does not need to do clever sibling collapse or parent gubbins, because in that case, the db forces tagsmanager refresh
|
|
# so this is just handling things if the content update has no sibling/parent tags
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_ACTUAL )
|
|
|
|
statuses_to_tags = service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
( data_type, action, row ) = content_update.ToTuple()
|
|
|
|
( tag, hashes ) = row
|
|
|
|
if action == HC.CONTENT_UPDATE_ADD:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ].add( tag )
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].discard( tag )
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].discard( tag )
|
|
|
|
elif action == HC.CONTENT_UPDATE_DELETE:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].add( tag )
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ].discard( tag )
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ].discard( tag )
|
|
|
|
elif action == HC.CONTENT_UPDATE_PEND:
|
|
|
|
if tag not in statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ]:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].add( tag )
|
|
|
|
|
|
elif action == HC.CONTENT_UPDATE_RESCIND_PEND:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_PENDING ].discard( tag )
|
|
|
|
elif action == HC.CONTENT_UPDATE_CLEAR_DELETE_RECORD:
|
|
|
|
statuses_to_tags[ HC.CONTENT_STATUS_DELETED ].discard( tag )
|
|
|
|
|
|
#
|
|
|
|
self._SetDirty()
|
|
|
|
|
|
|
|
def ResetService( self, service_key ):
|
|
|
|
with self._lock:
|
|
|
|
service_keys_to_statuses_to_tags = self._GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_STORAGE )
|
|
|
|
if service_key in service_keys_to_statuses_to_tags:
|
|
|
|
del service_keys_to_statuses_to_tags[ service_key ]
|
|
|
|
self._SetDirty()
|
|
|
|
|
|
|
|
|