Version 432
This commit is contained in:
parent
50641ab95d
commit
d56cb43f07
|
@ -8,6 +8,42 @@
|
|||
<div class="content">
|
||||
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
|
||||
<ul>
|
||||
<li><h3 id="version_432"><a href="#version_432">version 432</a></h3></li>
|
||||
<ul>
|
||||
<li>tag sorting:</li>
|
||||
<li>the tag sort dropdown has been replaced with a dynamic control. rather than one big list with all possible permutations, you now work on each variable (sort type, asc/desc, group by) separately. what you are actually sorting is easier to understand and select</li>
|
||||
<li>my stupid "lexicographic/incidence" labelling is replaced with the simpler and neater 'tag', 'subtag', and 'count'</li>
|
||||
<li>when in the manage tags dialog and sorting by tag or subtag, you can now turn off the 'use sibling' sort.</li>
|
||||
<li>I'd like to further neaten the workflow here, making the individual dropdowns flip back and forth with a mouse scroll in either directior rather than being just up/down allowed. let me know overall how you find this new control</li>
|
||||
<li>the 'tag sort' object is updated behind the scenes as well. your old value should be converted automatically</li>
|
||||
<li>fixed an issue with count tag sorting where deleted tag counts were being counted even when not displayed</li>
|
||||
<li>if you try to search tags on a page of thumbnails that holds an invalid tag, this is now caught gracefully and you get a little popup saying 'please run the repair invalid tags routine'</li>
|
||||
<li>.</li>
|
||||
<li>misc:</li>
|
||||
<li>the client now gives a once-per-boot warning popup if your session size exceeds 500k. for those who cannot reduce session size conveniently, this popup can be turned off under _options->gui pages_</li>
|
||||
<li>when the file import options prohibit a file due to filesize or resolution etc.., it should now always record that as an 'ignored' result rather than an 'error'</li>
|
||||
<li>fixed an unusual error popup in thread watcher display that could occur during session load. this problem seems to have been around for a long time, but it required a watcher in a previously saved and still valid 'wait a bit' error state and was only vulnerable for a few milliseconds, so it hadn't come up before. in any case, it is fixed</li>
|
||||
<li>subscriptions with small 'first run' file limits now work better: if you create a subscription with a fairly small 'first run' file limit (this typically matters when the number is smaller than one of the site's gallery page's worth of results), subsequent normal checks with larger file limits will be more aggressive about noticing that they 'caught up' to that small initial sync (previously, they would sometimes incorrectly think the site just got some files tagged out of order and bump right past that initial 'already in db' batch and keep going until they hit their own file limit)</li>
|
||||
<li>.</li>
|
||||
<li>advanced string processing:</li>
|
||||
<li>added a String Selector/Slicer object to the parsing system. this object allows you to select the nth item in a list of parsed strings or the mth to nth items. m can be 'start' and n can be 'end', and negative indices are allowed for both. pair it with the new Sorter for some neat new tricks!</li>
|
||||
<li>the string processing edit UI is now _more_ multi-string-aware. the test panel has had a code cleanup pass and now has a list of all the starting strings in the test data (e.g. all the urls parsed by the formula that launched the UI) rather than just the first, and a list of all results from that list. selecting any of the starting strings populates the 'single string' area, so you can now zoom in on one particular string to see what is happening to it</li>
|
||||
<li>the String Sorter edit UI now gets all the strings at that stage of processing, so you can review the sort properly</li>
|
||||
<li>the new String Slicer edit UI similarly gets all the strings at that stage of processing</li>
|
||||
<li>future updates will expand multi-string presentation and testing. I'd like to show the whole list at each stage</li>
|
||||
<li>.</li>
|
||||
<li>server/client api core improvements:</li>
|
||||
<li>I had a go at supporting the Range header for file (basically this means anything non-html/json) requests. I added tests and it seems to work. as I understand this mostly applies to browsers pulling video from the Client API. to start, I am supporting single range requests. if it is needed, I'll try to get Multi Range requests right, but for now they'll 416</li>
|
||||
<li>the client now understands 416 ("can't do that requested range m8") errors</li>
|
||||
<li>I reworked the serverside error handling chain. this has been borked for a long time due to my own lack of understanding of twisted's deferred system, and certain late-stage errors were just not being handled right. the server should no longer hang on these and now should print error info correctly, including a rough 500 in true late emergencies, and terminate the connection correctly</li>
|
||||
<li>.</li>
|
||||
<li>boring:</li>
|
||||
<li>fixed up a handful of typo-borked unit tests</li>
|
||||
<li>fixed my ordinal (xst, xnd, xrd, xth) text generator to deal with 11, 12, and 13 correctly lmao</li>
|
||||
<li>started some db maintenance routines and logistics to recover definitions and remove orphans in future, I feel great about it so far, but it'll have to wait for more of my db 'modules' refactoring to be more useful</li>
|
||||
<li>updated the mpv dll on the Windows release to 2021-02-28, it may improve some video support/performance</li>
|
||||
<li>updated sqlite dll on the Windows release to 3.34.1</li>
|
||||
</ul>
|
||||
<li><h3 id="version_431"><a href="#version_431">version 431</a></h3></li>
|
||||
<ul>
|
||||
<li>misc:</li>
|
||||
|
|
|
@ -274,17 +274,6 @@ SHUTDOWN_TIMESTAMP_VACUUM = 0
|
|||
SHUTDOWN_TIMESTAMP_FATTEN_AC_CACHE = 1
|
||||
SHUTDOWN_TIMESTAMP_DELETE_ORPHANS = 2
|
||||
|
||||
SORT_BY_LEXICOGRAPHIC_ASC = 8
|
||||
SORT_BY_LEXICOGRAPHIC_DESC = 9
|
||||
SORT_BY_INCIDENCE_ASC = 10
|
||||
SORT_BY_INCIDENCE_DESC = 11
|
||||
SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC = 12
|
||||
SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC = 13
|
||||
SORT_BY_INCIDENCE_NAMESPACE_ASC = 14
|
||||
SORT_BY_INCIDENCE_NAMESPACE_DESC = 15
|
||||
SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_ASC = 16
|
||||
SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_DESC = 17
|
||||
|
||||
SORT_FILES_BY_FILESIZE = 0
|
||||
SORT_FILES_BY_DURATION = 1
|
||||
SORT_FILES_BY_IMPORT_TIME = 2
|
||||
|
|
|
@ -74,7 +74,6 @@ def GetClientDefaultOptions():
|
|||
options[ 'confirm_client_exit' ] = False
|
||||
|
||||
options[ 'default_tag_repository' ] = CC.DEFAULT_LOCAL_TAG_SERVICE_KEY
|
||||
options[ 'default_tag_sort' ] = CC.SORT_BY_LEXICOGRAPHIC_ASC
|
||||
|
||||
options[ 'pause_export_folders_sync' ] = False
|
||||
options[ 'pause_import_folders_sync' ] = False
|
||||
|
|
|
@ -644,7 +644,9 @@ class SidecarExporter( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
all_tags = list( all_tags )
|
||||
|
||||
ClientTagSorting.SortTags( CC.SORT_BY_LEXICOGRAPHIC_DESC, all_tags )
|
||||
tag_sort = ClientTagSorting.TagSort.STATICGetTextASCDefault()
|
||||
|
||||
ClientTagSorting.SortTags( tag_sort, all_tags )
|
||||
|
||||
txt_path = os.path.join( directory, filename + '.txt' )
|
||||
|
||||
|
|
|
@ -232,6 +232,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
self._dictionary[ 'booleans' ][ 'expand_parents_on_storage_taglists' ] = True
|
||||
self._dictionary[ 'booleans' ][ 'expand_parents_on_storage_autocomplete_taglists' ] = True
|
||||
|
||||
self._dictionary[ 'booleans' ][ 'show_session_size_warnings' ] = True
|
||||
|
||||
#
|
||||
|
||||
self._dictionary[ 'colours' ] = HydrusSerialisable.SerialisableDictionary()
|
||||
|
@ -612,6 +614,12 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
self._dictionary[ 'default_collect' ] = ClientMedia.MediaCollect()
|
||||
|
||||
#
|
||||
|
||||
from hydrus.client.metadata import ClientTagSorting
|
||||
|
||||
self._dictionary[ 'default_tag_sort' ] = ClientTagSorting.TagSort.STATICGetTextASCDefault()
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
|
@ -897,6 +905,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
def GetDefaultTagSort( self ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
return self._dictionary[ 'default_tag_sort' ]
|
||||
|
||||
|
||||
|
||||
def GetDefaultWatcherCheckerOptions( self ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -1285,6 +1301,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
def SetDefaultTagSort( self, tag_sort ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._dictionary[ 'default_tag_sort' ] = tag_sort
|
||||
|
||||
|
||||
|
||||
def SetDefaultWatcherCheckerOptions( self, checker_options ):
|
||||
|
||||
with self._lock:
|
||||
|
|
|
@ -3710,6 +3710,165 @@ class StringMatch( StringProcessingStep ):
|
|||
|
||||
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_MATCH ] = StringMatch
|
||||
|
||||
class StringSlicer( StringProcessingStep ):
|
||||
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_SLICER
|
||||
SERIALISABLE_NAME = 'String Selector/Slicer'
|
||||
SERIALISABLE_VERSION = 1
|
||||
|
||||
def __init__( self, index_start: typing.Optional[ int ] = None, index_end: typing.Optional[ int ] = None ):
|
||||
|
||||
StringProcessingStep.__init__( self )
|
||||
|
||||
self._index_start = index_start
|
||||
self._index_end = index_end
|
||||
|
||||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
return ( self._index_start, self._index_end )
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
( self._index_start, self._index_end ) = serialisable_info
|
||||
|
||||
|
||||
def GetIndexStartEnd( self ) -> typing.Tuple[ typing.Optional[ int ], typing.Optional[ int ] ]:
|
||||
|
||||
return ( self._index_start, self._index_end )
|
||||
|
||||
|
||||
def MakesChanges( self ) -> bool:
|
||||
|
||||
return self._index_start is not None or self._index_end is not None
|
||||
|
||||
|
||||
def SelectsNothingEver( self ) -> bool:
|
||||
|
||||
if self._index_end == 0:
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if self._index_start is None or self._index_end is None:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
both_positive = self._index_start >= 0 and self._index_end >= 0
|
||||
both_negative = self._index_start < 0 and self._index_end < 0
|
||||
|
||||
if both_positive or both_negative:
|
||||
|
||||
if self._index_start >= self._index_end:
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def SelectsOne( self ) -> bool:
|
||||
|
||||
if self.SelectsNothingEver():
|
||||
|
||||
return False
|
||||
|
||||
|
||||
if self._index_start == -1 and self._index_end is None:
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if self._index_start is None or self._index_end is None:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
both_positive = self._index_start >= 0 and self._index_end >= 0
|
||||
both_negative = self._index_start < 0 and self._index_end < 0
|
||||
|
||||
return ( both_positive or both_negative ) and self._index_start == self._index_end - 1
|
||||
|
||||
|
||||
def Slice( self, texts: typing.Sequence[ str ] ) -> typing.List[ str ]:
|
||||
|
||||
try:
|
||||
|
||||
if self._index_start is None and self._index_end is None:
|
||||
|
||||
return list( texts )
|
||||
|
||||
elif self._index_end is None:
|
||||
|
||||
return texts[ self._index_start : ]
|
||||
|
||||
elif self._index_start is None:
|
||||
|
||||
return texts[ : self._index_end ]
|
||||
|
||||
else:
|
||||
|
||||
return texts[ self._index_start : self._index_end ]
|
||||
|
||||
|
||||
except IndexError as e:
|
||||
|
||||
return []
|
||||
|
||||
|
||||
|
||||
def ToString( self, simple = False, with_type = False ) -> str:
|
||||
|
||||
if simple:
|
||||
|
||||
return 'selector/slicer'
|
||||
|
||||
|
||||
if self.SelectsNothingEver():
|
||||
|
||||
result = 'selecting nothing'
|
||||
|
||||
elif self.SelectsOne():
|
||||
|
||||
result = 'selecting the {} string'.format( HydrusData.ConvertIndexToPrettyOrdinalString( self._index_start ) )
|
||||
|
||||
elif self._index_start is None and self._index_end is None:
|
||||
|
||||
result = 'selecting everything'
|
||||
|
||||
elif self._index_end is None:
|
||||
|
||||
result = 'selecting the {} string and onwards'.format( HydrusData.ConvertIndexToPrettyOrdinalString( self._index_start ) )
|
||||
|
||||
elif self._index_start is None:
|
||||
|
||||
result = 'selecting up to and including the {} string'.format( HydrusData.ConvertIndexToPrettyOrdinalString( self._index_end - 1 ) )
|
||||
|
||||
else:
|
||||
|
||||
result = 'selecting the {} string up to and including the {} string'.format( HydrusData.ConvertIndexToPrettyOrdinalString( self._index_start ), HydrusData.ConvertIndexToPrettyOrdinalString( self._index_end - 1 ) )
|
||||
|
||||
|
||||
if with_type:
|
||||
|
||||
if self.SelectsOne():
|
||||
|
||||
result = 'SELECT: {}'.format( result )
|
||||
|
||||
else:
|
||||
|
||||
result = 'SLICE: {}'.format( result )
|
||||
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_SLICER ] = StringSlicer
|
||||
|
||||
sort_str_enum = {
|
||||
CONTENT_PARSER_SORT_TYPE_NONE : 'no sorting',
|
||||
CONTENT_PARSER_SORT_TYPE_LEXICOGRAPHIC : 'strict lexicographic',
|
||||
|
@ -3974,7 +4133,7 @@ class StringProcessor( HydrusSerialisable.SerialisableBase ):
|
|||
return proc_strings
|
||||
|
||||
|
||||
def ProcessStrings( self, starting_strings: typing.Iterable[ str ], max_steps_allowed = None ) -> typing.List[ str ]:
|
||||
def ProcessStrings( self, starting_strings: typing.Iterable[ str ], max_steps_allowed = None, no_slicing = False ) -> typing.List[ str ]:
|
||||
|
||||
current_strings = list( starting_strings )
|
||||
|
||||
|
@ -3996,6 +4155,24 @@ class StringProcessor( HydrusSerialisable.SerialisableBase ):
|
|||
next_strings = current_strings
|
||||
|
||||
|
||||
elif isinstance( processing_step, StringSlicer ):
|
||||
|
||||
if no_slicing:
|
||||
|
||||
next_strings = current_strings
|
||||
|
||||
else:
|
||||
|
||||
try:
|
||||
|
||||
next_strings = processing_step.Slice( current_strings )
|
||||
|
||||
except:
|
||||
|
||||
next_strings = current_strings
|
||||
|
||||
|
||||
|
||||
else:
|
||||
|
||||
next_strings = []
|
||||
|
@ -4096,6 +4273,11 @@ class StringProcessor( HydrusSerialisable.SerialisableBase ):
|
|||
components.append( 'sorting' )
|
||||
|
||||
|
||||
if True in ( isinstance( ps, StringSlicer ) for ps in self._processing_steps ):
|
||||
|
||||
components.append( 'selecting/slicing' )
|
||||
|
||||
|
||||
return 'some {}'.format( ', '.join( components ) )
|
||||
|
||||
|
||||
|
|
|
@ -10736,9 +10736,25 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
tag_ids_to_full_counts = {}
|
||||
|
||||
showed_bad_tag_error = False
|
||||
|
||||
for ( i, ( tag, ( current_count, pending_count ) ) ) in enumerate( tags_to_counts.items() ):
|
||||
|
||||
tag_id = self.modules_tags.GetTagId( tag )
|
||||
try:
|
||||
|
||||
tag_id = self.modules_tags.GetTagId( tag )
|
||||
|
||||
except HydrusExceptions.TagSizeException:
|
||||
|
||||
if not showed_bad_tag_error:
|
||||
|
||||
showed_bad_tag_error = True
|
||||
|
||||
HydrusData.ShowText( 'Hey, you seem to have an invalid tag in view right now! Please run the \'repair invalid tags\' routine under the \'database\' menu asap!' )
|
||||
|
||||
|
||||
continue
|
||||
|
||||
|
||||
tag_ids_to_full_counts[ tag_id ] = ( current_count, max_current_count, pending_count, max_pending_count )
|
||||
|
||||
|
@ -15108,6 +15124,97 @@ class DB( HydrusDB.HydrusDB ):
|
|||
return result
|
||||
|
||||
|
||||
def _RecoverFromMissingDefinitions( self, content_type ):
|
||||
|
||||
# this is not finished, but basics are there
|
||||
# remember this func uses a bunch of similar tech for the eventual orphan definition cleansing routine
|
||||
# we just have to extend modules functionality to cover all content tables and we are good to go
|
||||
|
||||
if content_type == HC.CONTENT_TYPE_HASH:
|
||||
|
||||
definition_column_name = 'hash_id'
|
||||
|
||||
|
||||
# eventually migrate this gubbins to cancellable async done in parts, which means generating, handling, and releasing the temp table name more cleverly
|
||||
|
||||
# job presentation to UI
|
||||
|
||||
all_tables_and_columns = []
|
||||
|
||||
for module in self._modules:
|
||||
|
||||
all_tables_and_columns.extend( module.GetTablesAndColumnsThatUseDefinitions( HC.CONTENT_TYPE_HASH ) )
|
||||
|
||||
|
||||
temp_all_useful_definition_ids_table_name = 'durable_temp.all_useful_definition_ids_{}'.format( os.urandom( 8 ).hex() )
|
||||
|
||||
self._c.execute( 'CREATE TABLE {} ( {} INTEGER PRIMARY KEY );'.format( temp_all_useful_definition_ids_table_name, definition_column_name ) )
|
||||
|
||||
try:
|
||||
|
||||
num_to_do = 0
|
||||
|
||||
for ( table_name, column_name ) in all_tables_and_columns:
|
||||
|
||||
query = 'INSERT OR IGNORE INTO {} ( {} ) SELECT DISTINCT {} FROM {};'.format(
|
||||
temp_all_useful_definition_ids_table_name,
|
||||
definition_column_name,
|
||||
column_name,
|
||||
table_name
|
||||
)
|
||||
|
||||
self._c.execute( query )
|
||||
|
||||
num_to_do += self._GetRowCount()
|
||||
|
||||
|
||||
num_missing = 0
|
||||
num_recovered = 0
|
||||
|
||||
batch_of_definition_ids = self._c.execute( 'SELECT {} FROM {} LIMIT 1024;'.format( definition_column_name, temp_all_useful_definition_ids_table_name ) )
|
||||
|
||||
while len( batch_of_definition_ids ) > 1024:
|
||||
|
||||
for definition_id in batch_of_definition_ids:
|
||||
|
||||
if not self.modules_hashes.HasHashId( definition_id ):
|
||||
|
||||
if content_type == HC.CONTENT_TYPE_HASH and self.modules_hashes_local_cache.HasHashId( definition_id ):
|
||||
|
||||
hash = self.modules_hashes_local_cache.GetHash( definition_id )
|
||||
|
||||
self._c.execute( 'INSERT OR IGNORE INTO hashes ( hash_id, hash ) VALUES ( ?, ? );', ( definition_id, sqlite3.Binary( hash ) ) )
|
||||
|
||||
HydrusData.Print( '{} {} had no master definition, but I was able to recover from the local cache'.format( definition_column_name, definition_id ) )
|
||||
|
||||
num_recovered += 1
|
||||
|
||||
else:
|
||||
|
||||
HydrusData.Print( '{} {} had no master definition, it has been purged from the database!'.format( definition_column_name, definition_id ) )
|
||||
|
||||
for ( table_name, column_name ) in all_tables_and_columns:
|
||||
|
||||
self._c.execute( 'DELETE FROM {} WHERE {} = ?;'.format( table_name, column_name ), ( definition_id, ) )
|
||||
|
||||
|
||||
# tell user they will want to run clear orphan files, reset service cache info, and may need to recalc some autocomplete counts depending on total missing definitions
|
||||
# I should clear service info based on content_type
|
||||
|
||||
num_missing += 1
|
||||
|
||||
|
||||
|
||||
|
||||
batch_of_definition_ids = self._c.execute( 'SELECT {} FROM {} LIMIT 1024;'.format( definition_column_name, temp_all_useful_definition_ids_table_name ) )
|
||||
|
||||
|
||||
finally:
|
||||
|
||||
self._c.execute( 'DROP TABLE {};'.format( temp_all_useful_definition_ids_table_name ) )
|
||||
|
||||
|
||||
|
||||
def _RegenerateLocalHashCache( self ):
|
||||
|
||||
job_key = ClientThreading.JobKey( cancellable = True )
|
||||
|
@ -19200,6 +19307,85 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
|
||||
|
||||
if version == 431:
|
||||
|
||||
try:
|
||||
|
||||
new_options = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_CLIENT_OPTIONS )
|
||||
|
||||
old_options = self._GetOptions()
|
||||
|
||||
SORT_BY_LEXICOGRAPHIC_ASC = 8
|
||||
SORT_BY_LEXICOGRAPHIC_DESC = 9
|
||||
SORT_BY_INCIDENCE_ASC = 10
|
||||
SORT_BY_INCIDENCE_DESC = 11
|
||||
SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC = 12
|
||||
SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC = 13
|
||||
SORT_BY_INCIDENCE_NAMESPACE_ASC = 14
|
||||
SORT_BY_INCIDENCE_NAMESPACE_DESC = 15
|
||||
SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_ASC = 16
|
||||
SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_DESC = 17
|
||||
|
||||
old_default_tag_sort = old_options[ 'default_tag_sort' ]
|
||||
|
||||
from hydrus.client.metadata import ClientTagSorting
|
||||
|
||||
sort_type = ClientTagSorting.SORT_BY_HUMAN_TAG
|
||||
|
||||
if old_default_tag_sort in ( SORT_BY_LEXICOGRAPHIC_ASC, SORT_BY_LEXICOGRAPHIC_DESC, SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC, SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC ):
|
||||
|
||||
sort_type = ClientTagSorting.SORT_BY_HUMAN_TAG
|
||||
|
||||
elif old_default_tag_sort in ( SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_ASC, SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_DESC ):
|
||||
|
||||
sort_type = ClientTagSorting.SORT_BY_HUMAN_SUBTAG
|
||||
|
||||
elif old_default_tag_sort in ( SORT_BY_INCIDENCE_ASC, SORT_BY_INCIDENCE_DESC, SORT_BY_INCIDENCE_NAMESPACE_ASC, SORT_BY_INCIDENCE_NAMESPACE_DESC ):
|
||||
|
||||
sort_type = ClientTagSorting.SORT_BY_COUNT
|
||||
|
||||
|
||||
if old_default_tag_sort in ( SORT_BY_INCIDENCE_ASC, SORT_BY_INCIDENCE_NAMESPACE_ASC, SORT_BY_LEXICOGRAPHIC_ASC, SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_ASC, SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC ):
|
||||
|
||||
sort_order = CC.SORT_ASC
|
||||
|
||||
else:
|
||||
|
||||
sort_order = CC.SORT_DESC
|
||||
|
||||
|
||||
use_siblings = True
|
||||
|
||||
if old_default_tag_sort in ( SORT_BY_INCIDENCE_NAMESPACE_ASC, SORT_BY_INCIDENCE_NAMESPACE_DESC, SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC, SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC ):
|
||||
|
||||
group_by = ClientTagSorting.GROUP_BY_NAMESPACE
|
||||
|
||||
else:
|
||||
|
||||
group_by = ClientTagSorting.GROUP_BY_NOTHING
|
||||
|
||||
|
||||
tag_sort = ClientTagSorting.TagSort(
|
||||
sort_type = sort_type,
|
||||
sort_order = sort_order,
|
||||
use_siblings = use_siblings,
|
||||
group_by = group_by
|
||||
)
|
||||
|
||||
new_options.SetDefaultTagSort( tag_sort )
|
||||
|
||||
self.modules_serialisable.SetJSONDump( new_options )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
message = 'Trying to convert your old default tag sort to the new format failed! Please set it again in the options.'
|
||||
|
||||
self.pub_initial_message( message )
|
||||
|
||||
|
||||
|
||||
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
|
||||
|
||||
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
|
||||
|
|
|
@ -182,6 +182,20 @@ class ClientDBCacheLocalHashes( HydrusDBModule.HydrusDBModule ):
|
|||
return hash_ids_to_hashes
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
# we actually provide a backup, which we may want to automate later in mappings caches etc...
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def HasHashId( self, hash_id: int ):
|
||||
|
||||
result = self._c.execute( 'SELECT 1 FROM local_hashes_cache WHERE hash_id = ?;', ( hash_id, ) ).fetchone()
|
||||
|
||||
return result is not None
|
||||
|
||||
|
||||
class ClientDBCacheLocalTags( HydrusDBModule.HydrusDBModule ):
|
||||
|
||||
def __init__( self, cursor: sqlite3.Cursor, modules_tags: ClientDBMaster.ClientDBMasterTags ):
|
||||
|
@ -276,6 +290,13 @@ class ClientDBCacheLocalTags( HydrusDBModule.HydrusDBModule ):
|
|||
return expected_table_names
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
# we actually provide a backup, which we may want to automate later in mappings caches etc...
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def GetTag( self, tag_id ) -> str:
|
||||
|
||||
self._PopulateTagIdsToTagsCache( ( tag_id, ) )
|
||||
|
|
|
@ -129,6 +129,16 @@ class ClientDBFilesMetadataBasic( HydrusDBModule.HydrusDBModule ):
|
|||
return sum( ( 1 for mime in result if mime in HC.SEARCHABLE_MIMES ) )
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
if HC.CONTENT_TYPE_HASH:
|
||||
|
||||
return [ ( 'files_info', 'hash_id' ) ]
|
||||
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def GetTotalSize( self, hash_ids: typing.Collection[ int ] ) -> int:
|
||||
|
||||
if len( hash_ids ) == 1:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import sqlite3
|
||||
import typing
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusData
|
||||
from hydrus.core import HydrusDB
|
||||
from hydrus.core import HydrusDBModule
|
||||
|
@ -78,3 +79,45 @@ class ClientDBMappingsStorage( HydrusDBModule.HydrusDBModule ):
|
|||
self._CreateIndex( petitioned_mappings_table_name, [ 'hash_id', 'tag_id' ], unique = True )
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
if HC.CONTENT_TYPE_HASH:
|
||||
|
||||
tables_and_columns = []
|
||||
|
||||
for service_id in self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES ):
|
||||
|
||||
( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateMappingsTableNames( service_id )
|
||||
|
||||
tables_and_columns.extend( [
|
||||
( current_mappings_table_name, 'hash_id' ),
|
||||
( deleted_mappings_table_name, 'hash_id' ),
|
||||
( pending_mappings_table_name, 'hash_id' ),
|
||||
( petitioned_mappings_table_name, 'hash_id' )
|
||||
] )
|
||||
|
||||
|
||||
return tables_and_columns
|
||||
|
||||
elif HC.CONTENT_TYPE_TAG:
|
||||
|
||||
tables_and_columns = []
|
||||
|
||||
for service_id in self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES ):
|
||||
|
||||
( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateMappingsTableNames( service_id )
|
||||
|
||||
tables_and_columns.extend( [
|
||||
( current_mappings_table_name, 'tag_id' ),
|
||||
( deleted_mappings_table_name, 'tag_id' ),
|
||||
( pending_mappings_table_name, 'tag_id' ),
|
||||
( petitioned_mappings_table_name, 'tag_id' )
|
||||
] )
|
||||
|
||||
|
||||
return tables_and_columns
|
||||
|
||||
|
||||
return []
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import os
|
|||
import sqlite3
|
||||
import typing
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusData
|
||||
from hydrus.core import HydrusDB
|
||||
from hydrus.core import HydrusDBModule
|
||||
|
@ -282,6 +283,16 @@ class ClientDBMasterHashes( HydrusDBModule.HydrusDBModule ):
|
|||
return hash_ids_to_hashes
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
if HC.CONTENT_TYPE_HASH:
|
||||
|
||||
return [ ( 'local_hashes', 'hash_id' ) ]
|
||||
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def HasExtraHashes( self, hash_id ):
|
||||
|
||||
result = self._c.execute( 'SELECT 1 FROM local_hashes WHERE hash_id = ?;', ( hash_id, ) ).fetchone()
|
||||
|
@ -289,6 +300,13 @@ class ClientDBMasterHashes( HydrusDBModule.HydrusDBModule ):
|
|||
return result is not None
|
||||
|
||||
|
||||
def HasHashId( self, hash_id: int ):
|
||||
|
||||
result = self._c.execute( 'SELECT 1 FROM hashes WHERE hash_id = ?;', ( hash_id, ) ).fetchone()
|
||||
|
||||
return result is not None
|
||||
|
||||
|
||||
def SetExtraHashes( self, hash_id, md5, sha1, sha512 ):
|
||||
|
||||
self._c.execute( 'INSERT OR IGNORE INTO local_hashes ( hash_id, md5, sha1, sha512 ) VALUES ( ?, ?, ?, ? );', ( hash_id, sqlite3.Binary( md5 ), sqlite3.Binary( sha1 ), sqlite3.Binary( sha512 ) ) )
|
||||
|
@ -346,6 +364,11 @@ class ClientDBMasterTexts( HydrusDBModule.HydrusDBModule ):
|
|||
return label_id
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def GetText( self, text_id ):
|
||||
|
||||
result = self._c.execute( 'SELECT text FROM texts WHERE text_id = ?;', ( text_id, ) ).fetchone()
|
||||
|
@ -522,6 +545,13 @@ class ClientDBMasterTags( HydrusDBModule.HydrusDBModule ):
|
|||
return subtag_id
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
# maybe content type subtag/namespace, which would useful for bad subtags, although that's tricky because then the knock-on is killing tag definition rows
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def GetTag( self, tag_id ) -> str:
|
||||
|
||||
self._PopulateTagIdsToTagsCache( ( tag_id, ) )
|
||||
|
@ -539,6 +569,8 @@ class ClientDBMasterTags( HydrusDBModule.HydrusDBModule ):
|
|||
|
||||
except HydrusExceptions.TagSizeException:
|
||||
|
||||
# update this to instead go 'hey, does the dirty tag exist?' if it does, run the fix invalid tags routine
|
||||
|
||||
raise HydrusExceptions.TagSizeException( '"{}" tag seems not valid--when cleaned, it ends up with zero size!'.format( tag ) )
|
||||
|
||||
|
||||
|
@ -716,6 +748,13 @@ class ClientDBMasterURLs( HydrusDBModule.HydrusDBModule ):
|
|||
return expected_table_names
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
# if content type is a domain, then give urls? bleh
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def GetURLDomainId( self, domain ):
|
||||
|
||||
result = self._c.execute( 'SELECT domain_id FROM url_domains WHERE domain = ?;', ( domain, ) ).fetchone()
|
||||
|
|
|
@ -337,6 +337,11 @@ class ClientDBSerialisable( HydrusDBModule.HydrusDBModule ):
|
|||
return value
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def GetYAMLDump( self, dump_type, dump_name = None ):
|
||||
|
||||
if dump_name is None:
|
||||
|
|
|
@ -178,6 +178,11 @@ class ClientDBMasterServices( HydrusDBModule.HydrusDBModule ):
|
|||
return set( self._service_keys_to_service_ids.keys() )
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def UpdateService( self, service: ClientServices.Service ):
|
||||
|
||||
( service_key, service_type, name, dictionary ) = service.ToTuple()
|
||||
|
|
|
@ -324,6 +324,8 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
self._persistent_mpv_widgets = []
|
||||
|
||||
self._have_shown_session_size_warning = False
|
||||
|
||||
self._closed_pages = []
|
||||
|
||||
self._lock = threading.Lock()
|
||||
|
@ -5644,6 +5646,13 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
|
||||
self._last_total_page_weight = total_active_weight + total_closed_weight
|
||||
|
||||
if total_active_weight > 500000 and self._controller.new_options.GetBoolean( 'show_session_size_warnings' ) and not self._have_shown_session_size_warning:
|
||||
|
||||
self._have_shown_session_size_warning = True
|
||||
|
||||
HydrusData.ShowText( 'Your session weight is {}, which is pretty big! To keep your UI lag-free and avoid potential session saving problems that occur around 2 million weight, please try to close some pages or clear some finished downloaders!'.format( HydrusData.ToHumanInt( total_active_weight ) ) )
|
||||
|
||||
|
||||
ClientGUIMenus.AppendMenuLabel( menu, '{} pages open'.format( HydrusData.ToHumanInt( total_active_page_count ) ), 'You have this many pages open.' )
|
||||
ClientGUIMenus.AppendMenuLabel( menu, 'total session weight: {}'.format( HydrusData.ToHumanInt( self._last_total_page_weight ) ), 'Your session is this heavy.' )
|
||||
|
||||
|
|
|
@ -2001,7 +2001,9 @@ class CanvasWithDetails( Canvas ):
|
|||
|
||||
tags_i_want_to_display = list( tags_i_want_to_display )
|
||||
|
||||
ClientTagSorting.SortTags( HC.options[ 'default_tag_sort' ], tags_i_want_to_display )
|
||||
tag_sort = HG.client_controller.new_options.GetDefaultTagSort()
|
||||
|
||||
ClientTagSorting.SortTags( tag_sort, tags_i_want_to_display )
|
||||
|
||||
current_y = 3
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ from hydrus.client.gui import ClientGUIScrolledPanels
|
|||
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
|
||||
from hydrus.client.gui import ClientGUIShortcuts
|
||||
from hydrus.client.gui import ClientGUIStyle
|
||||
from hydrus.client.gui import ClientGUITagSorting
|
||||
from hydrus.client.gui import ClientGUITime
|
||||
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
||||
from hydrus.client.gui import QtPorting as QP
|
||||
|
@ -1389,6 +1390,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._number_of_gui_session_backups.setToolTip( 'The client keeps multiple rolling backups of your gui sessions. If you have very large sessions, you might like to reduce this number.' )
|
||||
|
||||
self._show_session_size_warnings = QW.QCheckBox( self._sessions_panel )
|
||||
|
||||
self._show_session_size_warnings.setToolTip( 'This will give you a once-per-boot warning popup if your active session contains more than 500k objects.' )
|
||||
|
||||
#
|
||||
|
||||
self._pages_panel = ClientGUICommon.StaticBox( self, 'pages' )
|
||||
|
@ -1473,6 +1478,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._number_of_gui_session_backups.setValue( self._new_options.GetInteger( 'number_of_gui_session_backups' ) )
|
||||
|
||||
self._show_session_size_warnings.setChecked( self._new_options.GetBoolean( 'show_session_size_warnings' ) )
|
||||
|
||||
self._default_new_page_goes.SetValue( self._new_options.GetInteger( 'default_new_page_goes' ) )
|
||||
|
||||
self._notebook_tab_alignment.SetValue( self._new_options.GetInteger( 'notebook_tab_alignment' ) )
|
||||
|
@ -1507,6 +1514,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
rows.append( ( 'If \'last session\' above, autosave it how often (minutes)?', self._last_session_save_period_minutes ) )
|
||||
rows.append( ( 'If \'last session\' above, only autosave during idle time?', self._only_save_last_session_during_idle ) )
|
||||
rows.append( ( 'Number of session backups to keep: ', self._number_of_gui_session_backups ) )
|
||||
rows.append( ( 'Show warning popup if session size exceeds 500k: ', self._show_session_size_warnings ) )
|
||||
|
||||
sessions_gridbox = ClientGUICommon.WrapInGrid( self._sessions_panel, rows )
|
||||
|
||||
|
@ -1582,6 +1590,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._new_options.SetInteger( 'number_of_gui_session_backups', self._number_of_gui_session_backups.value() )
|
||||
|
||||
self._new_options.SetBoolean( 'show_session_size_warnings', self._show_session_size_warnings.isChecked() )
|
||||
|
||||
self._new_options.SetBoolean( 'only_save_last_session_during_idle', self._only_save_last_session_during_idle.isChecked() )
|
||||
|
||||
self._new_options.SetInteger( 'default_new_page_goes', self._default_new_page_goes.GetValue() )
|
||||
|
@ -2914,18 +2924,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
general_panel = ClientGUICommon.StaticBox( self, 'general tag options' )
|
||||
|
||||
self._default_tag_sort = ClientGUICommon.BetterChoice( general_panel )
|
||||
|
||||
self._default_tag_sort.addItem( 'lexicographic (a-z)', CC.SORT_BY_LEXICOGRAPHIC_ASC )
|
||||
self._default_tag_sort.addItem( 'lexicographic (z-a)', CC.SORT_BY_LEXICOGRAPHIC_DESC )
|
||||
self._default_tag_sort.addItem( 'lexicographic (a-z) (group unnamespaced)', CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC )
|
||||
self._default_tag_sort.addItem( 'lexicographic (z-a) (group unnamespaced)', CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC )
|
||||
self._default_tag_sort.addItem( 'lexicographic (a-z) (ignore namespace)', CC.SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_ASC )
|
||||
self._default_tag_sort.addItem( 'lexicographic (z-a) (ignore namespace)', CC.SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_DESC )
|
||||
self._default_tag_sort.addItem( 'incidence (desc)', CC.SORT_BY_INCIDENCE_DESC )
|
||||
self._default_tag_sort.addItem( 'incidence (asc)', CC.SORT_BY_INCIDENCE_ASC )
|
||||
self._default_tag_sort.addItem( 'incidence (desc) (grouped by namespace)', CC.SORT_BY_INCIDENCE_NAMESPACE_DESC )
|
||||
self._default_tag_sort.addItem( 'incidence (asc) (grouped by namespace)', CC.SORT_BY_INCIDENCE_NAMESPACE_ASC )
|
||||
self._default_tag_sort = ClientGUITagSorting.TagSortControl( general_panel, self._new_options.GetDefaultTagSort(), show_siblings = True )
|
||||
|
||||
self._default_tag_repository = ClientGUICommon.BetterChoice( general_panel )
|
||||
|
||||
|
@ -2949,8 +2948,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
#
|
||||
|
||||
self._default_tag_sort.SetValue( HC.options[ 'default_tag_sort' ] )
|
||||
|
||||
self._default_tag_service_search_page.addItem( 'all known tags', CC.COMBINED_TAG_SERVICE_KEY )
|
||||
|
||||
services = HG.client_controller.services_manager.GetServices( HC.REAL_TAG_SERVICES )
|
||||
|
@ -3017,7 +3014,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
def UpdateOptions( self ):
|
||||
|
||||
HC.options[ 'default_tag_repository' ] = self._default_tag_repository.GetValue()
|
||||
HC.options[ 'default_tag_sort' ] = QP.GetClientData( self._default_tag_sort, self._default_tag_sort.currentIndex() )
|
||||
self._new_options.SetDefaultTagSort( self._default_tag_sort.GetValue() )
|
||||
|
||||
self._new_options.SetBoolean( 'expand_parents_on_storage_taglists', self._expand_parents_on_storage_taglists.isChecked() )
|
||||
self._new_options.SetBoolean( 'expand_parents_on_storage_autocomplete_taglists', self._expand_parents_on_storage_autocomplete_taglists.isChecked() )
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import re
|
||||
import typing
|
||||
|
||||
from qtpy import QtCore as QC
|
||||
from qtpy import QtWidgets as QW
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
|
@ -19,6 +20,285 @@ from hydrus.client.gui.lists import ClientGUIListBoxes
|
|||
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
|
||||
from hydrus.client.gui.lists import ClientGUIListCtrl
|
||||
|
||||
NO_RESULTS_TEXT = 'no results'
|
||||
|
||||
class MultilineStringConversionTestPanel( QW.QWidget ):
|
||||
|
||||
textSelected = QC.Signal( str )
|
||||
|
||||
def __init__( self, parent: QW.QWidget, string_processor: ClientParsing.StringProcessor ):
|
||||
|
||||
QW.QWidget.__init__( self, parent )
|
||||
|
||||
self._string_processor = string_processor
|
||||
|
||||
self._test_data = QW.QListWidget( self )
|
||||
|
||||
self._test_data.setSelectionMode( QW.QListWidget.SingleSelection )
|
||||
|
||||
self._result_data = QW.QListWidget( self )
|
||||
|
||||
self._result_data.setSelectionMode( QW.QListView.NoSelection )
|
||||
|
||||
#
|
||||
|
||||
left_vbox = QP.VBoxLayout()
|
||||
right_vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( left_vbox, ClientGUICommon.BetterStaticText( self, label = 'starting strings' ), CC.FLAGS_CENTER )
|
||||
QP.AddToLayout( left_vbox, self._test_data, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( right_vbox, ClientGUICommon.BetterStaticText( self, label = 'processed strings' ), CC.FLAGS_CENTER )
|
||||
QP.AddToLayout( right_vbox, self._result_data, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( hbox, left_vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, right_vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
|
||||
self.setLayout( hbox )
|
||||
|
||||
self._test_data.itemSelectionChanged.connect( self.EventSelection )
|
||||
|
||||
|
||||
def _GetStartingTexts( self ):
|
||||
|
||||
return [ self._test_data.item( i ).data( QC.Qt.UserRole ) for i in range( self._test_data.count() ) ]
|
||||
|
||||
|
||||
def _UpdateResults( self ):
|
||||
|
||||
texts = self._GetStartingTexts()
|
||||
|
||||
try:
|
||||
|
||||
results = self._string_processor.ProcessStrings( texts )
|
||||
|
||||
except HydrusExceptions.ParseException as e:
|
||||
|
||||
results = [ 'error in processing: {}'.format( e ) ]
|
||||
|
||||
|
||||
self._result_data.clear()
|
||||
|
||||
for ( insertion_index, result ) in enumerate( results ):
|
||||
|
||||
item = QW.QListWidgetItem()
|
||||
|
||||
item.setText( result )
|
||||
item.setData( QC.Qt.UserRole, result )
|
||||
|
||||
self._result_data.insertItem( insertion_index, item )
|
||||
|
||||
|
||||
|
||||
def EventSelection( self ):
|
||||
|
||||
items = self._test_data.selectedItems()
|
||||
|
||||
if len( items ) == 1:
|
||||
|
||||
( list_widget_item, ) = items
|
||||
|
||||
text = list_widget_item.data( QC.Qt.UserRole )
|
||||
|
||||
self.textSelected.emit( text )
|
||||
|
||||
|
||||
|
||||
def GetResultTexts( self, step_index ):
|
||||
|
||||
texts = self._GetStartingTexts()
|
||||
|
||||
try:
|
||||
|
||||
results = self._string_processor.ProcessStrings( texts, max_steps_allowed = step_index + 1 )
|
||||
|
||||
except:
|
||||
|
||||
results = []
|
||||
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def SetStringProcessor( self, string_processor: ClientParsing.StringProcessor ):
|
||||
|
||||
self._string_processor = string_processor
|
||||
|
||||
self._UpdateResults()
|
||||
|
||||
|
||||
def SetTestData( self, test_data: ClientParsing.ParsingTestData ):
|
||||
|
||||
self._test_data.clear()
|
||||
|
||||
for ( insertion_index, text ) in enumerate( test_data.texts ):
|
||||
|
||||
item = QW.QListWidgetItem()
|
||||
|
||||
item.setText( text )
|
||||
item.setData( QC.Qt.UserRole, text )
|
||||
|
||||
self._test_data.insertItem( insertion_index, item )
|
||||
|
||||
|
||||
self._UpdateResults()
|
||||
|
||||
if len( test_data.texts ) > 0:
|
||||
|
||||
self._test_data.item( 0 ).setSelected( False )
|
||||
self._test_data.item( 0 ).setSelected( True )
|
||||
|
||||
#self.textSelected.emit( self._test_data.item( 0 ).data( QC.Qt.UserRole ) )
|
||||
|
||||
|
||||
|
||||
class SingleStringConversionTestPanel( QW.QWidget ):
|
||||
|
||||
def __init__( self, parent: QW.QWidget, string_processor: ClientParsing.StringProcessor ):
|
||||
|
||||
QW.QWidget.__init__( self, parent )
|
||||
|
||||
self._string_processor = string_processor
|
||||
|
||||
self._example_string = QW.QLineEdit( self )
|
||||
|
||||
self._example_results = ClientGUICommon.BetterNotebook( self )
|
||||
|
||||
#
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText( self, label = 'single example string' ), CC.FLAGS_CENTER )
|
||||
QP.AddToLayout( vbox, self._example_string, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText( self, label = 'results for each step' ), CC.FLAGS_CENTER )
|
||||
QP.AddToLayout( vbox, self._example_results, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.setLayout( vbox )
|
||||
|
||||
self._example_string.textChanged.connect( self._UpdateResults )
|
||||
|
||||
|
||||
def _UpdateResults( self ):
|
||||
|
||||
processing_steps = self._string_processor.GetProcessingSteps()
|
||||
|
||||
current_selected_index = self._example_results.currentIndex()
|
||||
|
||||
self._example_results.DeleteAllPages()
|
||||
|
||||
example_string = self._example_string.text()
|
||||
|
||||
stop_now = False
|
||||
|
||||
for i in range( len( processing_steps ) ):
|
||||
|
||||
if isinstance( processing_steps[i], ClientParsing.StringSlicer ):
|
||||
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
|
||||
results = self._string_processor.ProcessStrings( [ example_string ], max_steps_allowed = i + 1, no_slicing = True )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
results = [ 'error: {}'.format( str( e ) ) ]
|
||||
|
||||
stop_now = True
|
||||
|
||||
|
||||
results_list = QW.QListWidget( self._example_results )
|
||||
results_list.setSelectionMode( QW.QListWidget.NoSelection )
|
||||
|
||||
if len( results ) == 0:
|
||||
|
||||
results_list.addItem( NO_RESULTS_TEXT )
|
||||
|
||||
stop_now = True
|
||||
|
||||
else:
|
||||
|
||||
for result in results:
|
||||
|
||||
if not isinstance( result, str ):
|
||||
|
||||
result = repr( result )
|
||||
|
||||
|
||||
results_list.addItem( result )
|
||||
|
||||
|
||||
|
||||
tab_label = '{} ({})'.format( processing_steps[i].ToString( simple = True ), HydrusData.ToHumanInt( len( results ) ) )
|
||||
|
||||
self._example_results.addTab( results_list, tab_label )
|
||||
|
||||
if stop_now:
|
||||
|
||||
break
|
||||
|
||||
|
||||
|
||||
if self._example_results.count() > current_selected_index:
|
||||
|
||||
self._example_results.setCurrentIndex( current_selected_index )
|
||||
|
||||
|
||||
|
||||
def GetResultText( self, step_index: int ):
|
||||
|
||||
example_text = self._example_string.text()
|
||||
|
||||
if 0 < step_index < self._example_results.count() + 1:
|
||||
|
||||
try:
|
||||
|
||||
t = self._example_results.widget( step_index - 1 ).item( 0 ).text()
|
||||
|
||||
if t != NO_RESULTS_TEXT:
|
||||
|
||||
example_text = t
|
||||
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
return example_text
|
||||
|
||||
|
||||
def GetStartingText( self ):
|
||||
|
||||
return self._example_string.text()
|
||||
|
||||
|
||||
def SetStringProcessor( self, string_processor: ClientParsing.StringProcessor ):
|
||||
|
||||
self._string_processor = string_processor
|
||||
|
||||
if True in ( isinstance( processing_step, ClientParsing.StringSlicer ) for processing_step in self._string_processor.GetProcessingSteps() ):
|
||||
|
||||
self.setToolTip( 'String Slicing is ignored here.' )
|
||||
|
||||
else:
|
||||
|
||||
self.setToolTip( '' )
|
||||
|
||||
|
||||
self._UpdateResults()
|
||||
|
||||
|
||||
def SetExampleString( self, example_string: str ):
|
||||
|
||||
self._example_string.setText( example_string )
|
||||
|
||||
self._UpdateResults()
|
||||
|
||||
|
||||
class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent: QW.QWidget, string_converter: ClientParsing.StringConverter, example_string_override = None ):
|
||||
|
@ -1011,6 +1291,199 @@ class EditStringMatchPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._UpdateControlVisibility()
|
||||
|
||||
|
||||
SELECT_SINGLE = 0
|
||||
SELECT_RANGE = 1
|
||||
|
||||
class EditStringSlicerPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, string_slicer: ClientParsing.StringSlicer, test_data: typing.Sequence[ str ] = [] ):
|
||||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
||||
#
|
||||
|
||||
self._controls_panel = ClientGUICommon.StaticBox( self, 'selector values' )
|
||||
|
||||
self._select_type = ClientGUICommon.BetterChoice( self._controls_panel )
|
||||
|
||||
self._select_type.addItem( 'select one item', SELECT_SINGLE )
|
||||
self._select_type.addItem( 'select range', SELECT_RANGE )
|
||||
|
||||
self._single_panel = QW.QWidget( self._controls_panel )
|
||||
|
||||
self._index_single = QP.MakeQSpinBox( self._single_panel, min = -65536, max = 65536 )
|
||||
|
||||
self._range_panel = QW.QWidget( self._controls_panel )
|
||||
|
||||
self._index_start = ClientGUICommon.NoneableSpinCtrl( self._range_panel, none_phrase = 'start at the beginning', min = -65536, max = 65536)
|
||||
self._index_end = ClientGUICommon.NoneableSpinCtrl( self._range_panel, none_phrase = 'finish at the end', min = -65536, max = 65536)
|
||||
|
||||
self._summary_st = ClientGUICommon.BetterStaticText( self._controls_panel )
|
||||
|
||||
#
|
||||
|
||||
self._example_panel = ClientGUICommon.StaticBox( self, 'test results' )
|
||||
|
||||
self._example_strings = QW.QListWidget( self._example_panel )
|
||||
self._example_strings.setSelectionMode( QW.QListWidget.NoSelection )
|
||||
|
||||
self._example_strings_sliced = QW.QListWidget( self._example_panel )
|
||||
self._example_strings_sliced.setSelectionMode( QW.QListWidget.NoSelection )
|
||||
|
||||
#
|
||||
|
||||
for s in test_data:
|
||||
|
||||
self._example_strings.addItem( s )
|
||||
|
||||
|
||||
self.SetValue( string_slicer )
|
||||
|
||||
#
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'index to select: ', self._index_single ) )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( self._single_panel, rows )
|
||||
|
||||
self._single_panel.setLayout( gridbox )
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'starting index: ', self._index_start ) )
|
||||
rows.append( ( 'ending index: ', self._index_end ) )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( self._range_panel, rows )
|
||||
|
||||
self._range_panel.setLayout( gridbox )
|
||||
|
||||
st = ClientGUICommon.BetterStaticText( self._controls_panel, label = 'Negative indices are ok! Check the summary text to make sure your numbers are correct!' )
|
||||
|
||||
st.setWordWrap( True )
|
||||
|
||||
self._controls_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._controls_panel.Add( self._select_type, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._controls_panel.Add( self._single_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._controls_panel.Add( self._range_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._controls_panel.Add( self._summary_st, CC.FLAGS_CENTER )
|
||||
|
||||
hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( hbox, self._example_strings, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, self._example_strings_sliced, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self._example_panel.Add( hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( vbox, self._controls_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._example_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.widget().setLayout( vbox )
|
||||
|
||||
#
|
||||
|
||||
self.SetValue( string_slicer )
|
||||
|
||||
self._select_type.currentIndexChanged.connect( self._ShowHideControls )
|
||||
|
||||
self._index_single.valueChanged.connect( self._UpdateControls )
|
||||
self._index_start.valueChanged.connect( self._UpdateControls )
|
||||
self._index_end.valueChanged.connect( self._UpdateControls )
|
||||
|
||||
|
||||
def _GetValue( self ):
|
||||
|
||||
select_type = self._select_type.GetValue()
|
||||
|
||||
if select_type == SELECT_SINGLE:
|
||||
|
||||
index_start = self._index_single.value()
|
||||
|
||||
if index_start == -1:
|
||||
|
||||
index_end = None
|
||||
|
||||
else:
|
||||
|
||||
index_end = index_start + 1
|
||||
|
||||
|
||||
elif select_type == SELECT_RANGE:
|
||||
|
||||
index_start = self._index_start.GetValue()
|
||||
index_end = self._index_end.GetValue()
|
||||
|
||||
|
||||
string_slicer = ClientParsing.StringSlicer( index_start = index_start, index_end = index_end )
|
||||
|
||||
return string_slicer
|
||||
|
||||
|
||||
def _ShowHideControls( self ):
|
||||
|
||||
select_type = self._select_type.GetValue()
|
||||
|
||||
self._single_panel.setVisible( select_type == SELECT_SINGLE )
|
||||
self._range_panel.setVisible( select_type == SELECT_RANGE )
|
||||
|
||||
self._UpdateControls()
|
||||
|
||||
|
||||
def _UpdateControls( self ):
|
||||
|
||||
string_slicer = self._GetValue()
|
||||
|
||||
self._summary_st.setText( string_slicer.ToString() )
|
||||
|
||||
texts = [ self._example_strings.item( i ).text() for i in range( self._example_strings.count() ) ]
|
||||
|
||||
try:
|
||||
|
||||
sliced_texts = string_slicer.Slice( texts )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
sliced_texts = [ 'Error: {}'.format( e ) ]
|
||||
|
||||
|
||||
self._example_strings_sliced.clear()
|
||||
|
||||
for s in sliced_texts:
|
||||
|
||||
self._example_strings_sliced.addItem( s )
|
||||
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
||||
string_slicer = self._GetValue()
|
||||
|
||||
return string_slicer
|
||||
|
||||
|
||||
def SetValue( self, string_slicer: ClientParsing.StringSlicer ):
|
||||
|
||||
( index_start, index_end ) = string_slicer.GetIndexStartEnd()
|
||||
|
||||
self._index_single.setValue( index_start if index_start is not None else 0 )
|
||||
|
||||
self._index_start.SetValue( index_start )
|
||||
self._index_end.SetValue( index_end )
|
||||
|
||||
if string_slicer.SelectsOne():
|
||||
|
||||
self._select_type.SetValue( SELECT_SINGLE )
|
||||
|
||||
else:
|
||||
|
||||
self._select_type.SetValue( SELECT_RANGE )
|
||||
|
||||
|
||||
self._ShowHideControls()
|
||||
|
||||
|
||||
class EditStringSorterPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, string_sorter: ClientParsing.StringSorter, test_data: typing.Sequence[ str ] = [] ):
|
||||
|
@ -1019,13 +1492,15 @@ class EditStringSorterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
#
|
||||
|
||||
self._controls_panel = ClientGUICommon.StaticBox( self, 'splitter values' )
|
||||
self._controls_panel = ClientGUICommon.StaticBox( self, 'sort values' )
|
||||
|
||||
self._sort_type = ClientGUICommon.BetterChoice( self._controls_panel )
|
||||
|
||||
self._sort_type.addItem( ClientParsing.sort_str_enum[ ClientParsing.CONTENT_PARSER_SORT_TYPE_HUMAN_SORT ], ClientParsing.CONTENT_PARSER_SORT_TYPE_HUMAN_SORT )
|
||||
self._sort_type.addItem( ClientParsing.sort_str_enum[ ClientParsing.CONTENT_PARSER_SORT_TYPE_LEXICOGRAPHIC ], ClientParsing.CONTENT_PARSER_SORT_TYPE_LEXICOGRAPHIC )
|
||||
|
||||
tt = 'Human sort sorts numbers as you understand them. "image 2" comes before "image 10". Lexicographic compares each character in turn. "image 02" comes before "image 10", which comes before "image 2".'
|
||||
|
||||
self._asc = QW.QCheckBox( self._controls_panel )
|
||||
|
||||
self._regex = ClientGUICommon.NoneableTextCtrl( self._controls_panel, none_phrase = 'use whole string' )
|
||||
|
@ -1074,13 +1549,6 @@ class EditStringSorterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
woah = 'Because string processing UI is still built around a single \'example\' string, the test UI here is bad! The whole system will be eventually updated to handle newlines and multiple example strings.'
|
||||
|
||||
st = ClientGUICommon.BetterStaticText( self, label = woah )
|
||||
|
||||
st.setWordWrap( True )
|
||||
|
||||
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._controls_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._example_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
|
@ -1152,9 +1620,9 @@ class EditStringSorterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
def GetValue( self ):
|
||||
|
||||
string_match = self._GetValue()
|
||||
string_sorter = self._GetValue()
|
||||
|
||||
return string_match
|
||||
return string_sorter
|
||||
|
||||
|
||||
def SetValue( self, string_sorter: ClientParsing.StringSorter ):
|
||||
|
@ -1259,9 +1727,9 @@ class EditStringSplitterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
def GetValue( self ):
|
||||
|
||||
string_match = self._GetValue()
|
||||
string_splitter = self._GetValue()
|
||||
|
||||
return string_match
|
||||
return string_splitter
|
||||
|
||||
|
||||
def SetValue( self, string_splitter: ClientParsing.StringSplitter ):
|
||||
|
@ -1277,8 +1745,6 @@ class EditStringSplitterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
NO_RESULTS_TEXT = 'no results'
|
||||
|
||||
def __init__( self, parent, string_processor: ClientParsing.StringProcessor, test_data: ClientParsing.ParsingTestData ):
|
||||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
@ -1293,9 +1759,11 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self._example_panel = ClientGUICommon.StaticBox( self, 'test results' )
|
||||
|
||||
self._example_string = QW.QLineEdit( self._example_panel )
|
||||
self._multiline_test_panel = MultilineStringConversionTestPanel( self._example_panel, string_processor )
|
||||
|
||||
self._example_results = ClientGUICommon.BetterNotebook( self._example_panel )
|
||||
self._single_test_panel = SingleStringConversionTestPanel( self._example_panel, string_processor )
|
||||
|
||||
#
|
||||
|
||||
( w, h ) = ClientGUIFunctions.ConvertTextToPixels( self._example_panel, ( 64, 24 ) )
|
||||
|
||||
|
@ -1303,41 +1771,31 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
#
|
||||
|
||||
if len( test_data.texts ) > 0:
|
||||
|
||||
example_string = test_data.texts[0]
|
||||
|
||||
else:
|
||||
example_string = ''
|
||||
|
||||
|
||||
self._example_string.setText( example_string )
|
||||
self._controls_panel.Add( self._processing_steps, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
|
||||
#
|
||||
|
||||
self._controls_panel.Add( self._processing_steps, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
example_hbox = QP.HBoxLayout()
|
||||
|
||||
rows = []
|
||||
QP.AddToLayout( example_hbox, self._multiline_test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( example_hbox, self._single_test_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
|
||||
rows.append( ( 'example string: ', self._example_string ) )
|
||||
self._example_panel.Add( example_hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( self._example_panel, rows )
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
self._example_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
self._example_panel.Add( ClientGUICommon.BetterStaticText( self, label = 'result:' ), CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._example_panel.Add( self._example_results, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, self._controls_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, self._example_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
hbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( hbox, self._controls_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, self._example_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.widget().setLayout( hbox )
|
||||
self.widget().setLayout( vbox )
|
||||
|
||||
#
|
||||
|
||||
self._processing_steps.listBoxChanged.connect( self._UpdateControls )
|
||||
self._example_string.textChanged.connect( self._UpdateControls )
|
||||
|
||||
self._multiline_test_panel.textSelected.connect( self._single_test_panel.SetExampleString )
|
||||
|
||||
self._multiline_test_panel.SetTestData( test_data )
|
||||
|
||||
self.SetValue( string_processor )
|
||||
|
||||
|
@ -1348,7 +1806,8 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
( 'String Match', ClientParsing.StringMatch, 'An object that filters strings.' ),
|
||||
( 'String Converter', ClientParsing.StringConverter, 'An object that converts strings from one thing to another.' ),
|
||||
( 'String Splitter', ClientParsing.StringSplitter, 'An object that breaks strings into smaller strings.' ),
|
||||
( 'String Sorter', ClientParsing.StringSorter, 'An object that sorts strings.' )
|
||||
( 'String Sorter', ClientParsing.StringSorter, 'An object that sorts strings.' ),
|
||||
( 'String Selector/Slicer', ClientParsing.StringSlicer, 'An object that filter-selects from the list of strings. Either absolute index position or a range.' )
|
||||
]
|
||||
|
||||
try:
|
||||
|
@ -1362,7 +1821,9 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
if string_processing_step_type == ClientParsing.StringMatch:
|
||||
|
||||
string_processing_step = ClientParsing.StringMatch( example_string = self._example_string.text() )
|
||||
example_text = self._single_test_panel.GetStartingText()
|
||||
|
||||
string_processing_step = ClientParsing.StringMatch( example_string = example_text )
|
||||
|
||||
example_text = self._GetExampleTextForStringProcessingStep( string_processing_step )
|
||||
|
||||
|
@ -1402,6 +1863,12 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
panel = EditStringSorterPanel( dlg, string_processing_step, test_data = test_data )
|
||||
|
||||
elif isinstance( string_processing_step, ClientParsing.StringSlicer ):
|
||||
|
||||
test_data = self._GetExampleTextsForStringSorter( string_processing_step )
|
||||
|
||||
panel = EditStringSlicerPanel( dlg, string_processing_step, test_data = test_data )
|
||||
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
|
@ -1425,7 +1892,7 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
def _GetExampleTextForStringProcessingStep( self, string_processing_step: ClientParsing.StringProcessingStep ):
|
||||
|
||||
# ultimately rework this to multiline test_data m8
|
||||
# ultimately rework this to multiline test_data m8, but the panels need it first
|
||||
|
||||
current_string_processor = self._GetValue()
|
||||
|
||||
|
@ -1440,24 +1907,7 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
example_text_index = len( current_string_processing_steps )
|
||||
|
||||
|
||||
example_text = self._example_string.text()
|
||||
|
||||
if 0 < example_text_index < self._example_results.count() + 1:
|
||||
|
||||
try:
|
||||
|
||||
t = self._example_results.widget( example_text_index - 1 ).item( 0 ).text()
|
||||
|
||||
if t != self.NO_RESULTS_TEXT:
|
||||
|
||||
example_text = t
|
||||
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
example_text = self._single_test_panel.GetResultText( example_text_index )
|
||||
|
||||
return example_text
|
||||
|
||||
|
@ -1479,26 +1929,7 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
example_text_index = len( current_string_processing_steps )
|
||||
|
||||
|
||||
example_texts = [ self._example_string.text() ]
|
||||
|
||||
if 0 < example_text_index < self._example_results.count() + 1:
|
||||
|
||||
try:
|
||||
|
||||
widget = self._example_results.widget( example_text_index - 1 )
|
||||
|
||||
example_texts = [ widget.item( i ).text() for i in range( widget.count() ) ]
|
||||
|
||||
if example_texts == [ self.NO_RESULTS_TEXT ]:
|
||||
|
||||
example_texts = [ self._example_string.text() ]
|
||||
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
example_texts = self._multiline_test_panel.GetResultTexts( example_text_index )
|
||||
|
||||
return example_texts
|
||||
|
||||
|
@ -1518,65 +1949,8 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
string_processor = self._GetValue()
|
||||
|
||||
processing_steps = string_processor.GetProcessingSteps()
|
||||
|
||||
current_selected_index = self._example_results.currentIndex()
|
||||
|
||||
self._example_results.DeleteAllPages()
|
||||
|
||||
example_string = self._example_string.text()
|
||||
|
||||
stop_now = False
|
||||
|
||||
for i in range( len( processing_steps ) ):
|
||||
|
||||
try:
|
||||
|
||||
results = string_processor.ProcessStrings( [ example_string ], max_steps_allowed = i + 1 )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
results = [ 'error: {}'.format( str( e ) ) ]
|
||||
|
||||
stop_now = True
|
||||
|
||||
|
||||
results_list = QW.QListWidget( self._example_panel )
|
||||
results_list.setSelectionMode( QW.QListWidget.NoSelection )
|
||||
|
||||
if len( results ) == 0:
|
||||
|
||||
results_list.addItem( self.NO_RESULTS_TEXT )
|
||||
|
||||
stop_now = True
|
||||
|
||||
else:
|
||||
|
||||
for result in results:
|
||||
|
||||
if not isinstance( result, str ):
|
||||
|
||||
result = repr( result )
|
||||
|
||||
|
||||
results_list.addItem( result )
|
||||
|
||||
|
||||
|
||||
tab_label = '{} ({})'.format( processing_steps[i].ToString( simple = True ), HydrusData.ToHumanInt( len( results ) ) )
|
||||
|
||||
self._example_results.addTab( results_list, tab_label )
|
||||
|
||||
if stop_now:
|
||||
|
||||
break
|
||||
|
||||
|
||||
|
||||
if self._example_results.count() > current_selected_index:
|
||||
|
||||
self._example_results.setCurrentIndex( current_selected_index )
|
||||
|
||||
self._multiline_test_panel.SetStringProcessor( string_processor )
|
||||
self._single_test_panel.SetStringProcessor( string_processor )
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
import typing
|
||||
|
||||
from qtpy import QtCore as QC
|
||||
from qtpy import QtWidgets as QW
|
||||
|
||||
from hydrus.client import ClientConstants as CC
|
||||
from hydrus.client.gui import ClientGUICommon
|
||||
from hydrus.client.gui import QtPorting as QP
|
||||
from hydrus.client.metadata import ClientTagSorting
|
||||
|
||||
class TagSortControl( QW.QWidget ):
|
||||
|
||||
valueChanged = QC.Signal()
|
||||
|
||||
def __init__( self, parent: QW.QWidget, tag_sort: ClientTagSorting.TagSort, show_siblings = False ):
|
||||
|
||||
QW.QWidget.__init__( self, parent )
|
||||
|
||||
self._sort_type = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
for sort_type in ( ClientTagSorting.SORT_BY_HUMAN_TAG, ClientTagSorting.SORT_BY_HUMAN_SUBTAG, ClientTagSorting.SORT_BY_COUNT ):
|
||||
|
||||
self._sort_type.addItem( ClientTagSorting.sort_type_str_lookup[ sort_type ], sort_type )
|
||||
|
||||
|
||||
self._sort_order_text = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
self._sort_order_text.addItem( 'a-z', CC.SORT_ASC )
|
||||
self._sort_order_text.addItem( 'z-a', CC.SORT_DESC )
|
||||
|
||||
self._sort_order_count = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
self._sort_order_count.addItem( 'most first', CC.SORT_DESC )
|
||||
self._sort_order_count.addItem( 'fewest first', CC.SORT_ASC )
|
||||
|
||||
self._show_siblings = show_siblings
|
||||
|
||||
self._use_siblings = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
self._use_siblings.addItem( 'siblings', True )
|
||||
self._use_siblings.addItem( 'tags', False )
|
||||
|
||||
self._group_by = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
self._group_by.addItem( 'no grouping', ClientTagSorting.GROUP_BY_NOTHING )
|
||||
self._group_by.addItem( 'group namespace', ClientTagSorting.GROUP_BY_NAMESPACE )
|
||||
|
||||
#
|
||||
|
||||
self.SetValue( tag_sort )
|
||||
|
||||
#
|
||||
|
||||
hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( hbox, self._sort_type, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, self._sort_order_text, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( hbox, self._sort_order_count, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( hbox, self._use_siblings, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( hbox, self._group_by, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
self.setLayout( hbox )
|
||||
|
||||
#
|
||||
|
||||
self._sort_type.currentIndexChanged.connect( self._UpdateControlsAfterSortTypeChanged )
|
||||
|
||||
self._sort_type.currentIndexChanged.connect( self._HandleValueChanged )
|
||||
self._sort_order_text.currentIndexChanged.connect( self._HandleValueChanged )
|
||||
self._sort_order_count.currentIndexChanged.connect( self._HandleValueChanged )
|
||||
self._use_siblings.currentIndexChanged.connect( self._HandleValueChanged )
|
||||
self._group_by.currentIndexChanged.connect( self._HandleValueChanged )
|
||||
|
||||
|
||||
def _UpdateControlsAfterSortTypeChanged( self ):
|
||||
|
||||
sort_type = self._sort_type.GetValue()
|
||||
|
||||
self._sort_order_text.setVisible( sort_type in ( ClientTagSorting.SORT_BY_HUMAN_TAG, ClientTagSorting.SORT_BY_HUMAN_SUBTAG ) )
|
||||
self._sort_order_count.setVisible( sort_type == ClientTagSorting.SORT_BY_COUNT )
|
||||
|
||||
self._use_siblings.setVisible( self._show_siblings and sort_type != ClientTagSorting.SORT_BY_COUNT )
|
||||
self._group_by.setVisible( sort_type != ClientTagSorting.SORT_BY_HUMAN_SUBTAG )
|
||||
|
||||
|
||||
def _HandleValueChanged( self ):
|
||||
|
||||
self.valueChanged.emit()
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
||||
sort_type = self._sort_type.GetValue()
|
||||
|
||||
if sort_type == ClientTagSorting.SORT_BY_COUNT:
|
||||
|
||||
sort_order = self._sort_order_count.GetValue()
|
||||
|
||||
else:
|
||||
|
||||
sort_order = self._sort_order_text.GetValue()
|
||||
|
||||
|
||||
tag_sort = ClientTagSorting.TagSort(
|
||||
sort_type = sort_type,
|
||||
sort_order = sort_order,
|
||||
use_siblings = self._use_siblings.GetValue(),
|
||||
group_by = self._group_by.GetValue()
|
||||
)
|
||||
|
||||
return tag_sort
|
||||
|
||||
|
||||
def SetValue( self, tag_sort: ClientTagSorting.TagSort ):
|
||||
|
||||
self._sort_type.blockSignals( True )
|
||||
self._sort_order_text.blockSignals( True )
|
||||
self._sort_order_count.blockSignals( True )
|
||||
self._use_siblings.blockSignals( True )
|
||||
self._group_by.blockSignals( True )
|
||||
|
||||
self._sort_type.SetValue( tag_sort.sort_type )
|
||||
|
||||
if tag_sort.sort_type == ClientTagSorting.SORT_BY_COUNT:
|
||||
|
||||
self._sort_order_count.SetValue( tag_sort.sort_order )
|
||||
|
||||
else:
|
||||
|
||||
self._sort_order_text.SetValue( tag_sort.sort_order )
|
||||
|
||||
|
||||
self._use_siblings.SetValue( tag_sort.use_siblings )
|
||||
self._group_by.SetValue( tag_sort.group_by )
|
||||
|
||||
self._sort_type.blockSignals( False )
|
||||
self._sort_order_text.blockSignals( False )
|
||||
self._sort_order_count.blockSignals( False )
|
||||
self._use_siblings.blockSignals( False )
|
||||
self._group_by.blockSignals( False )
|
||||
|
||||
self._UpdateControlsAfterSortTypeChanged()
|
||||
|
||||
self._HandleValueChanged()
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ class FavouritesTagsPanel( QW.QWidget ):
|
|||
|
||||
favourites = list( HG.client_controller.new_options.GetSuggestedTagsFavourites( self._service_key ) )
|
||||
|
||||
ClientTagSorting.SortTags( HC.options[ 'default_tag_sort' ], favourites )
|
||||
ClientTagSorting.SortTags( HG.client_controller.new_options.GetDefaultTagSort(), favourites )
|
||||
|
||||
tags = FilterSuggestedTagsForMedia( favourites, self._media, self._service_key )
|
||||
|
||||
|
|
|
@ -2009,7 +2009,7 @@ class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._i_am_local_tag_service = self._service.GetServiceType() == HC.LOCAL_TAG
|
||||
|
||||
self._tags_box_sorter = ClientGUIListBoxes.StaticBoxSorterForListBoxTags( self, 'tags' )
|
||||
self._tags_box_sorter = ClientGUIListBoxes.StaticBoxSorterForListBoxTags( self, 'tags', show_siblings_sort = True )
|
||||
|
||||
self._tags_box = ClientGUIListBoxes.ListBoxTagsMediaTagsDialog( self._tags_box_sorter, self.EnterTags, self.RemoveTags )
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ from hydrus.client.gui import ClientGUICore as CGC
|
|||
from hydrus.client.gui import ClientGUIFunctions
|
||||
from hydrus.client.gui import ClientGUIMenus
|
||||
from hydrus.client.gui import ClientGUIShortcuts
|
||||
from hydrus.client.gui import ClientGUITagSorting
|
||||
from hydrus.client.gui import QtPorting as QP
|
||||
from hydrus.client.gui.search import ClientGUISearch
|
||||
from hydrus.client.gui.lists import ClientGUIListBoxesData
|
||||
|
@ -2554,9 +2555,11 @@ class ListBoxTags( ListBox ):
|
|||
service_key_groups_to_tags[ tuple( s_ks ) ].append( t )
|
||||
|
||||
|
||||
tag_sort = ClientTagSorting.TagSort.STATICGetTextASCDefault()
|
||||
|
||||
for t_list in service_key_groups_to_tags.values():
|
||||
|
||||
ClientTagSorting.SortTags( CC.SORT_BY_LEXICOGRAPHIC_ASC, t_list )
|
||||
ClientTagSorting.SortTags( tag_sort, t_list )
|
||||
|
||||
|
||||
service_key_groups = sorted( service_key_groups_to_tags.keys(), key = lambda s_k_g: ( -len( s_k_g ), convert_service_keys_to_name_string( s_k_g ) ) )
|
||||
|
@ -2582,10 +2585,12 @@ class ListBoxTags( ListBox ):
|
|||
service_key_groups_to_tags[ tuple( s_ks ) ][1].append( c )
|
||||
|
||||
|
||||
tag_sort = ClientTagSorting.TagSort.STATICGetTextASCDefault()
|
||||
|
||||
for ( t_list_1, t_list_2 ) in service_key_groups_to_tags.values():
|
||||
|
||||
ClientTagSorting.SortTags( CC.SORT_BY_LEXICOGRAPHIC_ASC, t_list_1 )
|
||||
ClientTagSorting.SortTags( CC.SORT_BY_LEXICOGRAPHIC_ASC, t_list_2 )
|
||||
ClientTagSorting.SortTags( tag_sort, t_list_1 )
|
||||
ClientTagSorting.SortTags( tag_sort, t_list_2 )
|
||||
|
||||
|
||||
service_key_groups = sorted( service_key_groups_to_tags.keys(), key = lambda s_k_g: ( -len( s_k_g ), convert_service_keys_to_name_string( s_k_g ) ) )
|
||||
|
@ -3420,7 +3425,7 @@ class ListBoxTagsMedia( ListBoxTagsDisplayCapable ):
|
|||
|
||||
ListBoxTagsDisplayCapable.__init__( self, parent, service_key = service_key, tag_display_type = tag_display_type, height_num_chars = 24 )
|
||||
|
||||
self._sort = HC.options[ 'default_tag_sort' ]
|
||||
self._tag_sort = HG.client_controller.new_options.GetDefaultTagSort()
|
||||
|
||||
self._last_media = set()
|
||||
|
||||
|
@ -3521,7 +3526,7 @@ class ListBoxTagsMedia( ListBoxTagsDisplayCapable ):
|
|||
( self._show_petitioned, self._petitioned_tags_to_count )
|
||||
]
|
||||
|
||||
counts_to_include = [ c for ( show, c ) in jobs ]
|
||||
counts_to_include = [ c for ( show, c ) in jobs if show ]
|
||||
|
||||
for term in self._ordered_terms:
|
||||
|
||||
|
@ -3532,16 +3537,15 @@ class ListBoxTagsMedia( ListBoxTagsDisplayCapable ):
|
|||
terms_to_count[ term ] = count
|
||||
|
||||
|
||||
item_to_tag_key_wrapper = lambda term: term.GetTag()
|
||||
item_to_sibling_key_wrapper = item_to_tag_key_wrapper
|
||||
|
||||
if self._sibling_decoration_allowed:
|
||||
|
||||
item_to_tag_key_wrapper = lambda term: term.GetBestTag()
|
||||
|
||||
else:
|
||||
|
||||
item_to_tag_key_wrapper = lambda term: term.GetTag()
|
||||
item_to_sibling_key_wrapper = lambda term: term.GetBestTag()
|
||||
|
||||
|
||||
ClientTagSorting.SortTags( self._sort, self._ordered_terms, tag_items_to_count = terms_to_count, item_to_tag_key_wrapper = item_to_tag_key_wrapper )
|
||||
ClientTagSorting.SortTags( self._tag_sort, self._ordered_terms, tag_items_to_count = terms_to_count, item_to_tag_key_wrapper = item_to_tag_key_wrapper, item_to_sibling_key_wrapper = item_to_sibling_key_wrapper )
|
||||
|
||||
self._RegenTermsToIndices()
|
||||
|
||||
|
@ -3553,9 +3557,9 @@ class ListBoxTagsMedia( ListBoxTagsDisplayCapable ):
|
|||
self.SetTagsByMedia( self._last_media )
|
||||
|
||||
|
||||
def SetSort( self, sort ):
|
||||
def SetSort( self, tag_sort: ClientTagSorting.TagSort ):
|
||||
|
||||
self._sort = sort
|
||||
self._tag_sort = tag_sort
|
||||
|
||||
self._Sort()
|
||||
|
||||
|
@ -3686,28 +3690,16 @@ class ListBoxTagsMedia( ListBoxTagsDisplayCapable ):
|
|||
|
||||
class StaticBoxSorterForListBoxTags( ClientGUICommon.StaticBox ):
|
||||
|
||||
def __init__( self, parent, title ):
|
||||
def __init__( self, parent, title, show_siblings_sort = False ):
|
||||
|
||||
ClientGUICommon.StaticBox.__init__( self, parent, title )
|
||||
|
||||
self._sorter = ClientGUICommon.BetterChoice( self )
|
||||
# make this its own panel
|
||||
self._tag_sort = ClientGUITagSorting.TagSortControl( self, HG.client_controller.new_options.GetDefaultTagSort(), show_siblings = show_siblings_sort )
|
||||
|
||||
self._sorter.addItem( 'lexicographic (a-z)', CC.SORT_BY_LEXICOGRAPHIC_ASC )
|
||||
self._sorter.addItem( 'lexicographic (z-a)', CC.SORT_BY_LEXICOGRAPHIC_DESC )
|
||||
self._sorter.addItem( 'lexicographic (a-z) (group unnamespaced)', CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC )
|
||||
self._sorter.addItem( 'lexicographic (z-a) (group unnamespaced)', CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC )
|
||||
self._sorter.addItem( 'lexicographic (a-z) (ignore namespace)', CC.SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_ASC )
|
||||
self._sorter.addItem( 'lexicographic (z-a) (ignore namespace)', CC.SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_DESC )
|
||||
self._sorter.addItem( 'incidence (desc)', CC.SORT_BY_INCIDENCE_DESC )
|
||||
self._sorter.addItem( 'incidence (asc)', CC.SORT_BY_INCIDENCE_ASC )
|
||||
self._sorter.addItem( 'incidence (desc) (grouped by namespace)', CC.SORT_BY_INCIDENCE_NAMESPACE_DESC )
|
||||
self._sorter.addItem( 'incidence (asc) (grouped by namespace)', CC.SORT_BY_INCIDENCE_NAMESPACE_ASC )
|
||||
self._tag_sort.valueChanged.connect( self.EventSort )
|
||||
|
||||
self._sorter.SetValue( HC.options[ 'default_tag_sort' ] )
|
||||
|
||||
self._sorter.currentIndexChanged.connect( self.EventSort )
|
||||
|
||||
self.Add( self._sorter, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self.Add( self._tag_sort, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
|
||||
def SetTagServiceKey( self, service_key ):
|
||||
|
@ -3715,16 +3707,11 @@ class StaticBoxSorterForListBoxTags( ClientGUICommon.StaticBox ):
|
|||
self._tags_box.SetTagServiceKey( service_key )
|
||||
|
||||
|
||||
def EventSort( self, index ):
|
||||
def EventSort( self ):
|
||||
|
||||
selection = self._sorter.currentIndex()
|
||||
sort = self._tag_sort.GetValue()
|
||||
|
||||
if selection != -1:
|
||||
|
||||
sort = self._sorter.GetValue()
|
||||
|
||||
self._tags_box.SetSort( sort )
|
||||
|
||||
self._tags_box.SetSort( sort )
|
||||
|
||||
|
||||
def SetTagsBox( self, tags_box: ListBoxTagsMedia ):
|
||||
|
|
|
@ -90,23 +90,38 @@ class FileImportJob( object ):
|
|||
|
||||
self.GenerateInfo()
|
||||
|
||||
self.CheckIsGoodToImport()
|
||||
|
||||
mime = self.GetMime()
|
||||
|
||||
if status_hook is not None:
|
||||
try:
|
||||
|
||||
status_hook( 'copying file' )
|
||||
self.CheckIsGoodToImport()
|
||||
|
||||
ok_to_go = True
|
||||
|
||||
except HydrusExceptions.FileSizeException as e:
|
||||
|
||||
ok_to_go = False
|
||||
|
||||
import_status = CC.STATUS_SKIPPED
|
||||
note = str( e )
|
||||
|
||||
|
||||
HG.client_controller.client_files_manager.AddFile( hash, mime, self._temp_path, thumbnail_bytes = self._thumbnail_bytes )
|
||||
|
||||
if status_hook is not None:
|
||||
if ok_to_go:
|
||||
|
||||
status_hook( 'updating database' )
|
||||
mime = self.GetMime()
|
||||
|
||||
if status_hook is not None:
|
||||
|
||||
status_hook( 'copying file' )
|
||||
|
||||
|
||||
HG.client_controller.client_files_manager.AddFile( hash, mime, self._temp_path, thumbnail_bytes = self._thumbnail_bytes )
|
||||
|
||||
if status_hook is not None:
|
||||
|
||||
status_hook( 'updating database' )
|
||||
|
||||
|
||||
( import_status, note ) = HG.client_controller.WriteSynchronous( 'import_file', self )
|
||||
|
||||
|
||||
( import_status, note ) = HG.client_controller.WriteSynchronous( 'import_file', self )
|
||||
|
||||
else:
|
||||
|
||||
|
|
|
@ -659,6 +659,7 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ):
|
|||
gallery_seed_log = query_log_container.GetGallerySeedLog()
|
||||
|
||||
this_is_initial_sync = query_header.IsInitialSync()
|
||||
num_file_seeds_at_start = len( file_seed_cache )
|
||||
total_new_urls_for_this_sync = 0
|
||||
total_already_in_urls_for_this_sync = 0
|
||||
|
||||
|
@ -803,37 +804,55 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ):
|
|||
file_seeds_to_add_ordered.append( file_seed )
|
||||
|
||||
|
||||
if file_limit_for_this_sync is not None and total_new_urls_for_this_sync + num_urls_added >= file_limit_for_this_sync:
|
||||
if file_limit_for_this_sync is not None:
|
||||
|
||||
# we have found enough new files this sync, so should stop adding files and new gallery pages
|
||||
|
||||
if this_is_initial_sync:
|
||||
if total_new_urls_for_this_sync + num_urls_added >= file_limit_for_this_sync:
|
||||
|
||||
stop_reason = 'hit initial file limit'
|
||||
# we have found enough new files this sync, so should stop adding files and new gallery pages
|
||||
|
||||
else:
|
||||
|
||||
if total_already_in_urls_for_this_sync + num_urls_already_in_file_seed_cache > 0:
|
||||
if this_is_initial_sync:
|
||||
|
||||
# this sync produced some knowns, so it is likely we have stepped through a mix of old and tagged-late new files
|
||||
# we might also be on the second sync with a periodic limit greater than the initial limit
|
||||
# either way, this is no reason to go crying to the user
|
||||
|
||||
stop_reason = 'hit periodic file limit after seeing several already-seen files'
|
||||
stop_reason = 'hit initial file limit'
|
||||
|
||||
else:
|
||||
|
||||
# this page had all entirely new files
|
||||
|
||||
self._ShowHitPeriodicFileLimitMessage( query_name )
|
||||
|
||||
stop_reason = 'hit periodic file limit without seeing any already-seen files!'
|
||||
if total_already_in_urls_for_this_sync + num_urls_already_in_file_seed_cache > 0:
|
||||
|
||||
# this sync produced some knowns, so it is likely we have stepped through a mix of old and tagged-late new files
|
||||
# this is no reason to go crying to the user
|
||||
|
||||
stop_reason = 'hit periodic file limit after seeing several already-seen files'
|
||||
|
||||
else:
|
||||
|
||||
# this page had all entirely new files
|
||||
|
||||
self._ShowHitPeriodicFileLimitMessage( query_name )
|
||||
|
||||
stop_reason = 'hit periodic file limit without seeing any already-seen files!'
|
||||
|
||||
|
||||
|
||||
can_search_for_more_files = False
|
||||
|
||||
break
|
||||
|
||||
|
||||
can_search_for_more_files = False
|
||||
|
||||
if self._initial_file_limit is not None and self._periodic_file_limit is not None:
|
||||
|
||||
break
|
||||
# if the user has initial file sync of 5 but then normal sync of 100, we don't want to keep stomping through to older files on any subsequent normal sync
|
||||
# therefore, if we started this normal sync with fewer than normal sync files, we won't tolerate more than initial sync number of already in db
|
||||
if not this_is_initial_sync and num_file_seeds_at_start < self._periodic_file_limit and total_already_in_urls_for_this_sync >= self._initial_file_limit:
|
||||
|
||||
stop_reason = 'believe I caught up with initial sync'
|
||||
|
||||
can_search_for_more_files = False
|
||||
|
||||
# since most initial file limits will be > 5, this will likely be superceded immediately by the WE_HIT_OLD_GROUND_THRESHOLD bit in a sec, but whatever
|
||||
|
||||
break
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1230,7 +1230,14 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
elif not HydrusData.TimeHasPassed( self._no_work_until ):
|
||||
|
||||
text = '{} - next check {}'.format( self._no_work_until_reason, ClientData.TimestampToPrettyTimeDelta( max( self._no_work_until, self._next_check_time ) ) )
|
||||
if self._next_check_time is None:
|
||||
|
||||
text = '{} - working again {}'.format( self._no_work_until_reason, ClientData.TimestampToPrettyTimeDelta( self._no_work_until ) )
|
||||
|
||||
else:
|
||||
|
||||
text = '{} - next check {}'.format( self._no_work_until_reason, ClientData.TimestampToPrettyTimeDelta( max( self._no_work_until, self._next_check_time ) ) )
|
||||
|
||||
|
||||
return ( ClientImporting.DOWNLOADER_SIMPLE_STATUS_DEFERRED, text )
|
||||
|
||||
|
@ -1242,7 +1249,7 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
else:
|
||||
|
||||
if HydrusData.TimeHasPassed( self._next_check_time ):
|
||||
if self._next_check_time is None or HydrusData.TimeHasPassed( self._next_check_time ):
|
||||
|
||||
return ( ClientImporting.DOWNLOADER_SIMPLE_STATUS_PENDING, 'pending' )
|
||||
|
||||
|
@ -1271,7 +1278,14 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
elif not HydrusData.TimeHasPassed( self._no_work_until ):
|
||||
|
||||
no_work_text = '{} - next check {}'.format( self._no_work_until_reason, ClientData.TimestampToPrettyTimeDelta( max( self._no_work_until, self._next_check_time ) ) )
|
||||
if self._next_check_time is None:
|
||||
|
||||
no_work_text = '{} - working again {}'.format( self._no_work_until_reason, ClientData.TimestampToPrettyTimeDelta( self._no_work_until ) )
|
||||
|
||||
else:
|
||||
|
||||
no_work_text = '{} - next check {}'.format( self._no_work_until_reason, ClientData.TimestampToPrettyTimeDelta( max( self._no_work_until, self._next_check_time ) ) )
|
||||
|
||||
|
||||
file_status = no_work_text
|
||||
watcher_status = no_work_text
|
||||
|
|
|
@ -2945,7 +2945,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
else:
|
||||
|
||||
return ( 'ascending', 'descending', CC.SORT_BY_INCIDENCE_DESC )
|
||||
return ( 'ascending', 'descending', CC.SORT_DESC )
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,88 @@
|
|||
from hydrus.core import HydrusSerialisable
|
||||
from hydrus.core import HydrusTags
|
||||
|
||||
from hydrus.client import ClientConstants as CC
|
||||
|
||||
def SortTags( sort_by, list_of_tag_items, tag_items_to_count = None, item_to_tag_key_wrapper = None ):
|
||||
SORT_BY_HUMAN_TAG = 0
|
||||
SORT_BY_HUMAN_SUBTAG = 1
|
||||
SORT_BY_COUNT = 2
|
||||
|
||||
GROUP_BY_NOTHING = 0
|
||||
GROUP_BY_NAMESPACE = 1
|
||||
|
||||
sort_type_str_lookup = {
|
||||
SORT_BY_HUMAN_TAG : 'sort by tag',
|
||||
SORT_BY_HUMAN_SUBTAG : 'sort by subtag',
|
||||
SORT_BY_COUNT : 'sort by count'
|
||||
}
|
||||
|
||||
class TagSort( HydrusSerialisable.SerialisableBase ):
|
||||
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_TAG_SORT
|
||||
SERIALISABLE_NAME = 'Tag Sort'
|
||||
SERIALISABLE_VERSION = 1
|
||||
|
||||
def __init__( self, sort_type = None, sort_order = None, use_siblings = None, group_by = None ):
|
||||
|
||||
if sort_type is None:
|
||||
|
||||
sort_type = SORT_BY_HUMAN_TAG
|
||||
|
||||
|
||||
if sort_order is None:
|
||||
|
||||
sort_order = CC.SORT_ASC
|
||||
|
||||
|
||||
if use_siblings is None:
|
||||
|
||||
use_siblings = True
|
||||
|
||||
|
||||
if group_by is None:
|
||||
|
||||
group_by = GROUP_BY_NOTHING
|
||||
|
||||
|
||||
self.sort_type = sort_type
|
||||
self.sort_order = sort_order
|
||||
self.use_siblings = use_siblings
|
||||
self.group_by = group_by
|
||||
|
||||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
return ( self.sort_type, self.sort_order, self.use_siblings, self.group_by )
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
( self.sort_type, self.sort_order, self.use_siblings, self.group_by ) = serialisable_info
|
||||
|
||||
|
||||
def ToString( self ):
|
||||
|
||||
return '{} {}{}'.format(
|
||||
sort_type_str_lookup[ self.sort_type ],
|
||||
'asc' if self.sort_order == CC.SORT_ASC else 'desc',
|
||||
' namespace' if self.group_by == GROUP_BY_NAMESPACE else ''
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def STATICGetTextASCDefault() -> "TagSort":
|
||||
|
||||
return TagSort(
|
||||
sort_type = SORT_BY_HUMAN_TAG,
|
||||
sort_order = CC.SORT_ASC,
|
||||
use_siblings = True,
|
||||
group_by = GROUP_BY_NOTHING
|
||||
)
|
||||
|
||||
|
||||
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_TAG_SORT ] = TagSort
|
||||
|
||||
def SortTags( tag_sort: TagSort, list_of_tag_items, tag_items_to_count = None, item_to_tag_key_wrapper = None, item_to_sibling_key_wrapper = None ):
|
||||
|
||||
def lexicographic_key( tag ):
|
||||
|
||||
|
@ -72,16 +152,16 @@ def SortTags( sort_by, list_of_tag_items, tag_items_to_count = None, item_to_tag
|
|||
|
||||
sorts_to_do = []
|
||||
|
||||
if sort_by in ( CC.SORT_BY_INCIDENCE_ASC, CC.SORT_BY_INCIDENCE_DESC, CC.SORT_BY_INCIDENCE_NAMESPACE_ASC, CC.SORT_BY_INCIDENCE_NAMESPACE_DESC ):
|
||||
if tag_sort.sort_type == SORT_BY_COUNT:
|
||||
|
||||
# let's establish a-z here for equal incidence values later
|
||||
if sort_by in ( CC.SORT_BY_INCIDENCE_ASC, CC.SORT_BY_INCIDENCE_NAMESPACE_ASC ):
|
||||
if tag_sort.sort_order == CC.SORT_ASC:
|
||||
|
||||
sorts_to_do.append( ( lexicographic_key, True ) )
|
||||
|
||||
reverse = False
|
||||
|
||||
elif sort_by in ( CC.SORT_BY_INCIDENCE_DESC, CC.SORT_BY_INCIDENCE_NAMESPACE_DESC ):
|
||||
else:
|
||||
|
||||
sorts_to_do.append( ( lexicographic_key, False ) )
|
||||
|
||||
|
@ -90,15 +170,15 @@ def SortTags( sort_by, list_of_tag_items, tag_items_to_count = None, item_to_tag
|
|||
|
||||
sorts_to_do.append( ( incidence_key, reverse ) )
|
||||
|
||||
if sort_by in ( CC.SORT_BY_INCIDENCE_NAMESPACE_ASC, CC.SORT_BY_INCIDENCE_NAMESPACE_DESC ):
|
||||
if tag_sort.group_by == GROUP_BY_NAMESPACE:
|
||||
|
||||
# python list sort is stable, so lets now sort again
|
||||
|
||||
if sort_by == CC.SORT_BY_INCIDENCE_NAMESPACE_ASC:
|
||||
if tag_sort.sort_order == CC.SORT_ASC:
|
||||
|
||||
reverse = True
|
||||
|
||||
elif sort_by == CC.SORT_BY_INCIDENCE_NAMESPACE_DESC:
|
||||
else:
|
||||
|
||||
reverse = False
|
||||
|
||||
|
@ -108,27 +188,30 @@ def SortTags( sort_by, list_of_tag_items, tag_items_to_count = None, item_to_tag
|
|||
|
||||
else:
|
||||
|
||||
if sort_by in ( CC.SORT_BY_LEXICOGRAPHIC_DESC, CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC, CC.SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_DESC ):
|
||||
|
||||
reverse = True
|
||||
|
||||
elif sort_by in ( CC.SORT_BY_LEXICOGRAPHIC_ASC, CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC, CC.SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_ASC ):
|
||||
if tag_sort.sort_order == CC.SORT_ASC:
|
||||
|
||||
reverse = False
|
||||
|
||||
else:
|
||||
|
||||
reverse = True
|
||||
|
||||
|
||||
if sort_by in ( CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC, CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC ):
|
||||
|
||||
key = namespace_lexicographic_key
|
||||
|
||||
elif sort_by in ( CC.SORT_BY_LEXICOGRAPHIC_ASC, CC.SORT_BY_LEXICOGRAPHIC_DESC ):
|
||||
|
||||
key = lexicographic_key
|
||||
|
||||
elif sort_by in ( CC.SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_ASC, CC.SORT_BY_LEXICOGRAPHIC_IGNORE_NAMESPACE_DESC ):
|
||||
if tag_sort.sort_type == SORT_BY_HUMAN_SUBTAG:
|
||||
|
||||
key = subtag_lexicographic_key
|
||||
|
||||
else:
|
||||
|
||||
if tag_sort.group_by == GROUP_BY_NAMESPACE:
|
||||
|
||||
key = namespace_lexicographic_key
|
||||
|
||||
else:
|
||||
|
||||
key = lexicographic_key
|
||||
|
||||
|
||||
|
||||
sorts_to_do.append( ( key, reverse ) )
|
||||
|
||||
|
@ -137,9 +220,13 @@ def SortTags( sort_by, list_of_tag_items, tag_items_to_count = None, item_to_tag
|
|||
|
||||
key_to_use = key
|
||||
|
||||
if key_to_use != incidence_key: # other keys use tag, incidence uses tag item
|
||||
if key_to_use is not incidence_key: # other keys use tag, incidence uses tag item
|
||||
|
||||
if item_to_tag_key_wrapper is not None:
|
||||
if tag_sort.use_siblings and item_to_sibling_key_wrapper is not None:
|
||||
|
||||
key_to_use = lambda item: key( item_to_sibling_key_wrapper( item ) )
|
||||
|
||||
elif item_to_tag_key_wrapper is not None:
|
||||
|
||||
key_to_use = lambda item: key( item_to_tag_key_wrapper( item ) )
|
||||
|
||||
|
@ -147,4 +234,4 @@ def SortTags( sort_by, list_of_tag_items, tag_items_to_count = None, item_to_tag
|
|||
|
||||
list_of_tag_items.sort( key = key_to_use, reverse = reverse )
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -76,6 +76,10 @@ def ConvertStatusCodeAndDataIntoExceptionInfo( status_code, data, is_hydrus_serv
|
|||
|
||||
eclass = HydrusExceptions.ConflictException
|
||||
|
||||
elif status_code == 416:
|
||||
|
||||
eclass = HydrusExceptions.RangeNotSatisfiableException
|
||||
|
||||
elif status_code == 419:
|
||||
|
||||
eclass = HydrusExceptions.SessionException
|
||||
|
|
|
@ -70,7 +70,7 @@ options = {}
|
|||
# Misc
|
||||
|
||||
NETWORK_VERSION = 19
|
||||
SOFTWARE_VERSION = 431
|
||||
SOFTWARE_VERSION = 432
|
||||
CLIENT_API_VERSION = 15
|
||||
|
||||
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
|
||||
|
@ -139,6 +139,7 @@ CONTENT_TYPE_TIMESTAMP = 16
|
|||
CONTENT_TYPE_TITLE = 17
|
||||
CONTENT_TYPE_NOTES = 18
|
||||
CONTENT_TYPE_FILE_VIEWING_STATS = 19
|
||||
CONTENT_TYPE_TAG = 20
|
||||
|
||||
content_type_string_lookup = {}
|
||||
|
||||
|
|
|
@ -98,3 +98,10 @@ class HydrusDBModule( object ):
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
# could also do another one of these for orphan tables that have service id in the name.
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
|
|
|
@ -108,30 +108,46 @@ def ConvertIntToPrettyOrdinalString( num: int ):
|
|||
return 'unknown position'
|
||||
|
||||
|
||||
remainder = abs( num ) % 10
|
||||
tens = abs( num ) // 10
|
||||
|
||||
if remainder == 1:
|
||||
if tens == 1:
|
||||
|
||||
ordinal = 'st'
|
||||
|
||||
elif remainder == 2:
|
||||
|
||||
ordinal = 'nd'
|
||||
|
||||
elif remainder == 3:
|
||||
|
||||
ordinal = 'rd'
|
||||
ordinal = 'th'
|
||||
|
||||
else:
|
||||
|
||||
ordinal = 'th'
|
||||
remainder = abs( num ) % 10
|
||||
|
||||
if remainder == 1:
|
||||
|
||||
ordinal = 'st'
|
||||
|
||||
elif remainder == 2:
|
||||
|
||||
ordinal = 'nd'
|
||||
|
||||
elif remainder == 3:
|
||||
|
||||
ordinal = 'rd'
|
||||
|
||||
else:
|
||||
|
||||
ordinal = 'th'
|
||||
|
||||
|
||||
|
||||
s = '{}{}'.format( ToHumanInt( abs( num ) ), ordinal )
|
||||
|
||||
if num < 0:
|
||||
|
||||
s = '{} from last'.format( s )
|
||||
if num == -1:
|
||||
|
||||
s = 'last'
|
||||
|
||||
else:
|
||||
|
||||
s = '{} from last'.format( s )
|
||||
|
||||
|
||||
|
||||
return s
|
||||
|
|
|
@ -88,6 +88,7 @@ class NotFoundException( NetworkException ): pass
|
|||
class NotModifiedException( NetworkException ): pass
|
||||
class BadRequestException( NetworkException ): pass
|
||||
class ConflictException( NetworkException ): pass
|
||||
class RangeNotSatisfiableException( NetworkException ): pass
|
||||
class MissingCredentialsException( NetworkException ): pass
|
||||
class DoesNotSupportCORSException( NetworkException ): pass
|
||||
class InsufficientCredentialsException( NetworkException ): pass
|
||||
|
|
|
@ -119,6 +119,8 @@ SERIALISABLE_TYPE_NETWORK_SESSION_MANAGER_SESSION_CONTAINER = 96
|
|||
SERIALISABLE_TYPE_NETWORK_BANDWIDTH_MANAGER_TRACKER_CONTAINER = 97
|
||||
SERIALISABLE_TYPE_SIDECAR_EXPORTER = 98
|
||||
SERIALISABLE_TYPE_STRING_SORTER = 99
|
||||
SERIALISABLE_TYPE_STRING_SLICER = 100
|
||||
SERIALISABLE_TYPE_TAG_SORT = 101
|
||||
|
||||
SERIALISABLE_TYPES_TO_OBJECT_TYPES = {}
|
||||
|
||||
|
|
|
@ -4,9 +4,10 @@ import traceback
|
|||
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet.threads import deferToThread
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.web.server import NOT_DONE_YET
|
||||
from twisted.web.resource import Resource
|
||||
from twisted.web.static import File as FileResource, NoRangeStaticProducer
|
||||
from twisted.web.static import File as FileResource, NoRangeStaticProducer, SingleRangeStaticProducer, MultipleRangeStaticProducer
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusData
|
||||
|
@ -355,187 +356,6 @@ class HydrusResource( Resource ):
|
|||
return request
|
||||
|
||||
|
||||
def _callbackEstablishAccountFromHeader( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _callbackEstablishAccountFromArgs( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _callbackParseGETArgs( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _callbackParsePOSTArgs( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _checkService( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _checkUserAgent( self, request ):
|
||||
|
||||
request.is_hydrus_user_agent = False
|
||||
|
||||
if request.requestHeaders.hasHeader( 'User-Agent' ):
|
||||
|
||||
user_agent_texts = request.requestHeaders.getRawHeaders( 'User-Agent' )
|
||||
|
||||
user_agent_text = user_agent_texts[0]
|
||||
|
||||
try:
|
||||
|
||||
user_agents = user_agent_text.split( ' ' )
|
||||
|
||||
except:
|
||||
|
||||
return # crazy user agent string, so just assume not a hydrus client
|
||||
|
||||
|
||||
for user_agent in user_agents:
|
||||
|
||||
if '/' in user_agent:
|
||||
|
||||
( client, network_version ) = user_agent.split( '/', 1 )
|
||||
|
||||
if client == 'hydrus':
|
||||
|
||||
request.is_hydrus_user_agent = True
|
||||
|
||||
network_version = int( network_version )
|
||||
|
||||
if network_version == HC.NETWORK_VERSION:
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
|
||||
if network_version < HC.NETWORK_VERSION: message = 'Your client is out of date; please download the latest release.'
|
||||
else: message = 'This server is out of date; please ask its admin to update to the latest release.'
|
||||
|
||||
raise HydrusExceptions.NetworkVersionException( 'Network version mismatch! This server\'s network version is ' + str( HC.NETWORK_VERSION ) + ', whereas your client\'s is ' + str( network_version ) + '! ' + message )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _callbackRenderResponseContext( self, request ):
|
||||
|
||||
self._CleanUpTempFile( request )
|
||||
|
||||
if request.channel is None:
|
||||
|
||||
# Connection was lost, it seems.
|
||||
# no need for request.finish
|
||||
|
||||
return
|
||||
|
||||
|
||||
if request.requestHeaders.hasHeader( 'Origin' ):
|
||||
|
||||
if self._service.SupportsCORS():
|
||||
|
||||
request.setHeader( 'Access-Control-Allow-Origin', '*' )
|
||||
|
||||
|
||||
|
||||
response_context = request.hydrus_response_context
|
||||
|
||||
status_code = response_context.GetStatusCode()
|
||||
|
||||
request.setResponseCode( status_code )
|
||||
|
||||
for ( k, v, kwargs ) in response_context.GetCookies():
|
||||
|
||||
request.addCookie( k, v, **kwargs )
|
||||
|
||||
|
||||
do_finish = True
|
||||
|
||||
if response_context.HasPath():
|
||||
|
||||
path = response_context.GetPath()
|
||||
|
||||
size = os.path.getsize( path )
|
||||
|
||||
mime = response_context.GetMime()
|
||||
|
||||
content_type = HC.mime_mimetype_string_lookup[ mime ]
|
||||
|
||||
content_length = size
|
||||
|
||||
( base, filename ) = os.path.split( path )
|
||||
|
||||
content_disposition = 'inline; filename="' + filename + '"'
|
||||
|
||||
request.setHeader( 'Content-Type', str( content_type ) )
|
||||
request.setHeader( 'Content-Length', str( content_length ) )
|
||||
request.setHeader( 'Content-Disposition', str( content_disposition ) )
|
||||
|
||||
request.setHeader( 'Expires', time.strftime( '%a, %d %b %Y %H:%M:%S GMT', time.gmtime( time.time() + 86400 * 365 ) ) )
|
||||
request.setHeader( 'Cache-Control', 'max-age={}'.format( 86400 * 365 ) )
|
||||
|
||||
fileObject = open( path, 'rb' )
|
||||
|
||||
producer = NoRangeStaticProducer( request, fileObject )
|
||||
|
||||
producer.start()
|
||||
|
||||
do_finish = False
|
||||
|
||||
elif response_context.HasBody():
|
||||
|
||||
mime = response_context.GetMime()
|
||||
|
||||
body_bytes = response_context.GetBodyBytes()
|
||||
|
||||
content_type = HC.mime_mimetype_string_lookup[ mime ]
|
||||
|
||||
content_length = len( body_bytes )
|
||||
|
||||
content_disposition = 'inline'
|
||||
|
||||
request.setHeader( 'Content-Type', content_type )
|
||||
request.setHeader( 'Content-Length', str( content_length ) )
|
||||
request.setHeader( 'Content-Disposition', content_disposition )
|
||||
|
||||
request.write( body_bytes )
|
||||
|
||||
else:
|
||||
|
||||
content_length = 0
|
||||
|
||||
if status_code != 204: # 204 is No Content
|
||||
|
||||
request.setHeader( 'Content-Length', str( content_length ) )
|
||||
|
||||
|
||||
|
||||
self._reportDataUsed( request, content_length )
|
||||
self._reportRequestUsed( request )
|
||||
|
||||
if do_finish:
|
||||
|
||||
request.finish()
|
||||
|
||||
|
||||
|
||||
def _profileJob( self, call, request ):
|
||||
|
||||
HydrusData.Profile( 'client api {}'.format( request.path ), 'request.result_lmao = call( request )', globals(), locals(), min_duration_ms = 3, show_summary = True )
|
||||
|
||||
return request.result_lmao
|
||||
|
||||
|
||||
def _callbackDoGETJob( self, request ):
|
||||
|
||||
def wrap_thread_result( response_context ):
|
||||
|
@ -605,6 +425,233 @@ class HydrusResource( Resource ):
|
|||
return d
|
||||
|
||||
|
||||
def _callbackEstablishAccountFromHeader( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _callbackEstablishAccountFromArgs( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _callbackParseGETArgs( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _callbackParsePOSTArgs( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _callbackRenderResponseContext( self, request ):
|
||||
|
||||
self._CleanUpTempFile( request )
|
||||
|
||||
if request.channel is None:
|
||||
|
||||
# Connection was lost, it seems.
|
||||
# no need for request.finish
|
||||
|
||||
return
|
||||
|
||||
|
||||
if request.requestHeaders.hasHeader( 'Origin' ):
|
||||
|
||||
if self._service.SupportsCORS():
|
||||
|
||||
request.setHeader( 'Access-Control-Allow-Origin', '*' )
|
||||
|
||||
|
||||
|
||||
response_context = request.hydrus_response_context
|
||||
|
||||
if response_context.HasPath():
|
||||
|
||||
path = response_context.GetPath()
|
||||
|
||||
size = os.path.getsize( path )
|
||||
|
||||
offset_and_block_size_pairs = self._parseRangeHeader( request, size )
|
||||
|
||||
else:
|
||||
|
||||
offset_and_block_size_pairs = []
|
||||
|
||||
|
||||
status_code = response_context.GetStatusCode()
|
||||
|
||||
if status_code == 200 and response_context.HasPath() and len( offset_and_block_size_pairs ) > 0:
|
||||
|
||||
status_code = 206
|
||||
|
||||
|
||||
request.setResponseCode( status_code )
|
||||
|
||||
for ( k, v, kwargs ) in response_context.GetCookies():
|
||||
|
||||
request.addCookie( k, v, **kwargs )
|
||||
|
||||
|
||||
do_finish = True
|
||||
|
||||
if response_context.HasPath():
|
||||
|
||||
path = response_context.GetPath()
|
||||
|
||||
size = os.path.getsize( path )
|
||||
|
||||
mime = response_context.GetMime()
|
||||
|
||||
content_type = HC.mime_mimetype_string_lookup[ mime ]
|
||||
|
||||
( base, filename ) = os.path.split( path )
|
||||
|
||||
fileObject = open( path, 'rb' )
|
||||
|
||||
content_disposition = 'inline; filename="' + filename + '"'
|
||||
|
||||
request.setHeader( 'Content-Disposition', str( content_disposition ) )
|
||||
|
||||
request.setHeader( 'Expires', time.strftime( '%a, %d %b %Y %H:%M:%S GMT', time.gmtime( time.time() + 86400 * 365 ) ) )
|
||||
request.setHeader( 'Cache-Control', 'max-age={}'.format( 86400 * 365 ) )
|
||||
|
||||
if len( offset_and_block_size_pairs ) <= 1:
|
||||
|
||||
request.setHeader( 'Content-Type', str( content_type ) )
|
||||
|
||||
if len( offset_and_block_size_pairs ) == 0:
|
||||
|
||||
content_length = size
|
||||
|
||||
request.setHeader( 'Content-Length', str( content_length ) )
|
||||
|
||||
producer = NoRangeStaticProducer( request, fileObject )
|
||||
|
||||
elif len( offset_and_block_size_pairs ) == 1:
|
||||
|
||||
[ ( range_start, range_end, offset, block_size ) ] = offset_and_block_size_pairs
|
||||
|
||||
content_length = block_size
|
||||
|
||||
request.setHeader( 'Accept-Ranges', 'bytes' )
|
||||
request.setHeader( 'Content-Range', 'bytes {}-{}/{}'.format( offset, range_end, size ) )
|
||||
request.setHeader( 'Content-Length', str( content_length ) )
|
||||
|
||||
producer = SingleRangeStaticProducer( request, fileObject, offset, block_size )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
# hey, what a surprise, an http data transmission standard turned out to be a massive PITA
|
||||
# MultipleRangeStaticProducer is the lad to use, but you have to figure out your own separation bits, which have even more finicky rules. more than I can deal with with the current time I have
|
||||
# if/when you want to do this, check out the FileResource, it does it in its internal gubbins
|
||||
|
||||
raise HydrusExceptions.RangeNotSatisfiableException( 'Can only support Single Range requests at the moment!' )
|
||||
|
||||
|
||||
producer.start()
|
||||
|
||||
do_finish = False
|
||||
|
||||
elif response_context.HasBody():
|
||||
|
||||
mime = response_context.GetMime()
|
||||
|
||||
body_bytes = response_context.GetBodyBytes()
|
||||
|
||||
content_type = HC.mime_mimetype_string_lookup[ mime ]
|
||||
|
||||
content_length = len( body_bytes )
|
||||
|
||||
content_disposition = 'inline'
|
||||
|
||||
request.setHeader( 'Content-Type', content_type )
|
||||
request.setHeader( 'Content-Length', str( content_length ) )
|
||||
request.setHeader( 'Content-Disposition', content_disposition )
|
||||
|
||||
request.write( body_bytes )
|
||||
|
||||
else:
|
||||
|
||||
content_length = 0
|
||||
|
||||
if status_code != 204: # 204 is No Content
|
||||
|
||||
request.setHeader( 'Content-Length', str( content_length ) )
|
||||
|
||||
|
||||
|
||||
self._reportDataUsed( request, content_length )
|
||||
self._reportRequestUsed( request )
|
||||
|
||||
if do_finish:
|
||||
|
||||
request.finish()
|
||||
|
||||
|
||||
|
||||
def _checkService( self, request ):
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _checkUserAgent( self, request ):
|
||||
|
||||
request.is_hydrus_user_agent = False
|
||||
|
||||
if request.requestHeaders.hasHeader( 'User-Agent' ):
|
||||
|
||||
user_agent_texts = request.requestHeaders.getRawHeaders( 'User-Agent' )
|
||||
|
||||
user_agent_text = user_agent_texts[0]
|
||||
|
||||
try:
|
||||
|
||||
user_agents = user_agent_text.split( ' ' )
|
||||
|
||||
except:
|
||||
|
||||
return # crazy user agent string, so just assume not a hydrus client
|
||||
|
||||
|
||||
for user_agent in user_agents:
|
||||
|
||||
if '/' in user_agent:
|
||||
|
||||
( client, network_version ) = user_agent.split( '/', 1 )
|
||||
|
||||
if client == 'hydrus':
|
||||
|
||||
request.is_hydrus_user_agent = True
|
||||
|
||||
network_version = int( network_version )
|
||||
|
||||
if network_version == HC.NETWORK_VERSION:
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
|
||||
if network_version < HC.NETWORK_VERSION: message = 'Your client is out of date; please download the latest release.'
|
||||
else: message = 'This server is out of date; please ask its admin to update to the latest release.'
|
||||
|
||||
raise HydrusExceptions.NetworkVersionException( 'Network version mismatch! This server\'s network version is ' + str( HC.NETWORK_VERSION ) + ', whereas your client\'s is ' + str( network_version ) + '! ' + message )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _profileJob( self, call, request ):
|
||||
|
||||
HydrusData.Profile( 'client api {}'.format( request.path ), 'request.result_lmao = call( request )', globals(), locals(), min_duration_ms = 3, show_summary = True )
|
||||
|
||||
return request.result_lmao
|
||||
|
||||
|
||||
def _DecompressionBombsOK( self, request ):
|
||||
|
||||
return False
|
||||
|
@ -615,85 +662,91 @@ class HydrusResource( Resource ):
|
|||
request_deferred.cancel()
|
||||
|
||||
|
||||
def _errbackHandleEmergencyError( self, failure, request ):
|
||||
|
||||
try: self._CleanUpTempFile( request )
|
||||
except: pass
|
||||
|
||||
try: HydrusData.DebugPrint( failure.getTraceback() )
|
||||
except: pass
|
||||
|
||||
if request.channel is not None:
|
||||
|
||||
try: request.setResponseCode( 500 )
|
||||
except: pass
|
||||
|
||||
try: request.write( failure.getTraceback() )
|
||||
except: pass
|
||||
|
||||
|
||||
if not request.finished:
|
||||
|
||||
try: request.finish()
|
||||
except: pass
|
||||
|
||||
|
||||
|
||||
def _errbackHandleProcessingError( self, failure, request ):
|
||||
|
||||
self._CleanUpTempFile( request )
|
||||
|
||||
default_mime = HC.TEXT_HTML
|
||||
default_encoding = str
|
||||
|
||||
if failure.type == HydrusExceptions.BadRequestException:
|
||||
try:
|
||||
|
||||
response_context = ResponseContext( 400, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
try: self._CleanUpTempFile( request )
|
||||
except: pass
|
||||
|
||||
elif failure.type in ( HydrusExceptions.MissingCredentialsException, HydrusExceptions.DoesNotSupportCORSException ):
|
||||
default_mime = HC.TEXT_HTML
|
||||
default_encoding = str
|
||||
|
||||
response_context = ResponseContext( 401, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
if failure.type == HydrusExceptions.BadRequestException:
|
||||
|
||||
response_context = ResponseContext( 400, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type in ( HydrusExceptions.MissingCredentialsException, HydrusExceptions.DoesNotSupportCORSException ):
|
||||
|
||||
response_context = ResponseContext( 401, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.InsufficientCredentialsException:
|
||||
|
||||
response_context = ResponseContext( 403, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type in ( HydrusExceptions.NotFoundException, HydrusExceptions.DataMissing, HydrusExceptions.FileMissingException ):
|
||||
|
||||
response_context = ResponseContext( 404, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.ConflictException:
|
||||
|
||||
response_context = ResponseContext( 409, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.RangeNotSatisfiableException:
|
||||
|
||||
response_context = ResponseContext( 416, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.SessionException:
|
||||
|
||||
response_context = ResponseContext( 419, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.NetworkVersionException:
|
||||
|
||||
response_context = ResponseContext( 426, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.ServerBusyException:
|
||||
|
||||
response_context = ResponseContext( 503, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.BandwidthException:
|
||||
|
||||
response_context = ResponseContext( 509, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.ServerException:
|
||||
|
||||
response_context = ResponseContext( 500, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
else:
|
||||
|
||||
HydrusData.DebugPrint( failure.getTraceback() )
|
||||
|
||||
response_context = ResponseContext( 500, mime = default_mime, body = default_encoding( 'The repository encountered an error it could not handle! Here is a dump of what happened, which will also be written to your client.log file. If it persists, please forward it to hydrus.admin@gmail.com:' + os.linesep * 2 + failure.getTraceback() ) )
|
||||
|
||||
|
||||
elif failure.type == HydrusExceptions.InsufficientCredentialsException:
|
||||
request.hydrus_response_context = response_context
|
||||
|
||||
response_context = ResponseContext( 403, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
self._callbackRenderResponseContext( request )
|
||||
|
||||
elif failure.type in ( HydrusExceptions.NotFoundException, HydrusExceptions.DataMissing, HydrusExceptions.FileMissingException ):
|
||||
except:
|
||||
|
||||
response_context = ResponseContext( 404, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
try: HydrusData.DebugPrint( failure.getTraceback() )
|
||||
except: pass
|
||||
|
||||
elif failure.type == HydrusExceptions.ConflictException:
|
||||
if hasattr( request, 'channel' ) and request.channel is not None:
|
||||
|
||||
try: request.setResponseCode( 500 )
|
||||
except: pass
|
||||
|
||||
try: request.write( failure.getTraceback() )
|
||||
except: pass
|
||||
|
||||
|
||||
response_context = ResponseContext( 409, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
if not request.finished:
|
||||
|
||||
try: request.finish()
|
||||
except: pass
|
||||
|
||||
|
||||
elif failure.type == HydrusExceptions.SessionException:
|
||||
|
||||
response_context = ResponseContext( 419, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.NetworkVersionException:
|
||||
|
||||
response_context = ResponseContext( 426, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.ServerBusyException:
|
||||
|
||||
response_context = ResponseContext( 503, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.BandwidthException:
|
||||
|
||||
response_context = ResponseContext( 509, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
elif failure.type == HydrusExceptions.ServerException:
|
||||
|
||||
response_context = ResponseContext( 500, mime = default_mime, body = default_encoding( failure.value ) )
|
||||
|
||||
else:
|
||||
|
||||
HydrusData.DebugPrint( failure.getTraceback() )
|
||||
|
||||
response_context = ResponseContext( 500, mime = default_mime, body = default_encoding( 'The repository encountered an error it could not handle! Here is a dump of what happened, which will also be written to your client.log file. If it persists, please forward it to hydrus.admin@gmail.com:' + os.linesep * 2 + failure.getTraceback() ) )
|
||||
|
||||
|
||||
request.hydrus_response_context = response_context
|
||||
|
||||
return request
|
||||
|
||||
|
@ -728,6 +781,104 @@ class HydrusResource( Resource ):
|
|||
return access_key
|
||||
|
||||
|
||||
def _parseRangeHeader( self, request, size ):
|
||||
|
||||
offset_and_block_size_pairs = []
|
||||
|
||||
if request.requestHeaders.hasHeader( 'Range' ):
|
||||
|
||||
range_headers = request.requestHeaders.getRawHeaders( 'Range' )
|
||||
|
||||
range_header = range_headers[0]
|
||||
|
||||
if '=' not in range_header:
|
||||
|
||||
raise HydrusExceptions.BadRequestException( 'Did not understand range header!' )
|
||||
|
||||
|
||||
( unit_gumpf, range_pairs_string ) = range_header.split( '=', 1 )
|
||||
|
||||
if unit_gumpf != 'bytes':
|
||||
|
||||
raise HydrusExceptions.RangeNotSatisfiableException( 'Do not support anything other than bytes in Range header!' )
|
||||
|
||||
|
||||
range_pair_strings = range_pairs_string.split( ',' )
|
||||
|
||||
if True in ( '-' not in range_pair_string for range_pair_string in range_pair_strings ):
|
||||
|
||||
raise HydrusExceptions.RangeNotSatisfiableException( 'Did not understand the Range header\'s range pair(s)!' )
|
||||
|
||||
|
||||
range_pairs = [ range_pair_string.strip().split( '-' ) for range_pair_string in range_pair_strings ]
|
||||
|
||||
offset_and_block_size_pairs = []
|
||||
|
||||
for ( range_start, range_end ) in range_pairs:
|
||||
|
||||
if range_start == '':
|
||||
|
||||
if range_end == '':
|
||||
|
||||
raise HydrusExceptions.RangeNotSatisfiableException( 'Undefined Range header pair given!' )
|
||||
|
||||
|
||||
range_start = None
|
||||
|
||||
else:
|
||||
|
||||
range_start = abs( int( range_start ) )
|
||||
|
||||
|
||||
if range_end == '':
|
||||
|
||||
range_end = None
|
||||
|
||||
else:
|
||||
|
||||
range_end = abs( int( range_end ) )
|
||||
|
||||
|
||||
if range_start is not None and range_end is not None and range_start > range_end:
|
||||
|
||||
raise HydrusExceptions.RangeNotSatisfiableException( 'The Range header had an invalid pair!' )
|
||||
|
||||
|
||||
if range_start is None:
|
||||
|
||||
offset = size - range_end
|
||||
block_size = range_end
|
||||
|
||||
elif range_end is None:
|
||||
|
||||
offset = range_start
|
||||
block_size = size - range_start
|
||||
|
||||
else:
|
||||
|
||||
if range_start > size:
|
||||
|
||||
offset_and_block_size_pairs = []
|
||||
|
||||
break
|
||||
|
||||
|
||||
if range_end > size:
|
||||
|
||||
range_end = size - 1
|
||||
|
||||
|
||||
offset = range_start
|
||||
block_size = ( range_end + 1 ) - range_start
|
||||
|
||||
|
||||
offset_and_block_size_pairs.append( ( range_start, range_end, offset, block_size ) )
|
||||
|
||||
|
||||
|
||||
return offset_and_block_size_pairs
|
||||
|
||||
|
||||
def _reportDataUsed( self, request, num_bytes ):
|
||||
|
||||
self._service.ReportDataUsed( num_bytes )
|
||||
|
@ -827,11 +978,9 @@ class HydrusResource( Resource ):
|
|||
|
||||
d.addCallback( self._callbackDoGETJob )
|
||||
|
||||
d.addErrback( self._errbackHandleProcessingError, request )
|
||||
|
||||
d.addCallback( self._callbackRenderResponseContext )
|
||||
|
||||
d.addErrback( self._errbackHandleEmergencyError, request )
|
||||
d.addErrback( self._errbackHandleProcessingError, request )
|
||||
|
||||
request.notifyFinish().addErrback( self._errbackDisconnected, d )
|
||||
|
||||
|
@ -850,11 +999,9 @@ class HydrusResource( Resource ):
|
|||
|
||||
d.addCallback( self._callbackDoOPTIONSJob )
|
||||
|
||||
d.addErrback( self._errbackHandleProcessingError, request )
|
||||
|
||||
d.addCallback( self._callbackRenderResponseContext )
|
||||
|
||||
d.addErrback( self._errbackHandleEmergencyError, request )
|
||||
d.addErrback( self._errbackHandleProcessingError, request )
|
||||
|
||||
request.notifyFinish().addErrback( self._errbackDisconnected, d )
|
||||
|
||||
|
@ -881,11 +1028,9 @@ class HydrusResource( Resource ):
|
|||
|
||||
d.addCallback( self._callbackDoPOSTJob )
|
||||
|
||||
d.addErrback( self._errbackHandleProcessingError, request )
|
||||
|
||||
d.addCallback( self._callbackRenderResponseContext )
|
||||
|
||||
d.addErrback( self._errbackHandleEmergencyError, request )
|
||||
d.addErrback( self._errbackHandleProcessingError, request )
|
||||
|
||||
request.notifyFinish().addErrback( self._errbackDisconnected, d )
|
||||
|
||||
|
|
|
@ -2106,6 +2106,106 @@ class TestClientAPI( unittest.TestCase ):
|
|||
|
||||
self.assertEqual( hashlib.sha256( data ).digest(), hash )
|
||||
|
||||
# range request
|
||||
|
||||
path = '/get_files/file?file_id={}'.format( 1 )
|
||||
|
||||
partial_headers = dict( headers )
|
||||
partial_headers[ 'Range' ] = 'bytes=100-199'
|
||||
|
||||
connection.request( 'GET', path, headers = partial_headers )
|
||||
|
||||
response = connection.getresponse()
|
||||
|
||||
data = response.read()
|
||||
|
||||
self.assertEqual( response.status, 206 )
|
||||
|
||||
with open( file_path, 'rb' ) as f:
|
||||
|
||||
f.seek( 100 )
|
||||
|
||||
actual_data = f.read( 100 )
|
||||
|
||||
|
||||
self.assertEqual( data, actual_data )
|
||||
|
||||
# n onwards range request
|
||||
|
||||
path = '/get_files/file?file_id={}'.format( 1 )
|
||||
|
||||
partial_headers = dict( headers )
|
||||
partial_headers[ 'Range' ] = 'bytes=100-'
|
||||
|
||||
connection.request( 'GET', path, headers = partial_headers )
|
||||
|
||||
response = connection.getresponse()
|
||||
|
||||
data = response.read()
|
||||
|
||||
self.assertEqual( response.status, 206 )
|
||||
|
||||
with open( file_path, 'rb' ) as f:
|
||||
|
||||
f.seek( 100 )
|
||||
|
||||
actual_data = f.read()
|
||||
|
||||
|
||||
self.assertEqual( data, actual_data )
|
||||
|
||||
# last n onwards range request
|
||||
|
||||
path = '/get_files/file?file_id={}'.format( 1 )
|
||||
|
||||
partial_headers = dict( headers )
|
||||
partial_headers[ 'Range' ] = 'bytes=-100'
|
||||
|
||||
connection.request( 'GET', path, headers = partial_headers )
|
||||
|
||||
response = connection.getresponse()
|
||||
|
||||
data = response.read()
|
||||
|
||||
self.assertEqual( response.status, 206 )
|
||||
|
||||
with open( file_path, 'rb' ) as f:
|
||||
|
||||
actual_data = f.read()[-100:]
|
||||
|
||||
|
||||
self.assertEqual( data, actual_data )
|
||||
|
||||
# invalid range request
|
||||
|
||||
path = '/get_files/file?file_id={}'.format( 1 )
|
||||
|
||||
partial_headers = dict( headers )
|
||||
partial_headers[ 'Range' ] = 'bytes=200-199'
|
||||
|
||||
connection.request( 'GET', path, headers = partial_headers )
|
||||
|
||||
response = connection.getresponse()
|
||||
|
||||
data = response.read()
|
||||
|
||||
self.assertEqual( response.status, 416 )
|
||||
|
||||
# multi range request, not currently supported
|
||||
|
||||
path = '/get_files/file?file_id={}'.format( 1 )
|
||||
|
||||
partial_headers = dict( headers )
|
||||
partial_headers[ 'Range' ] = 'bytes=100-199,300-399'
|
||||
|
||||
connection.request( 'GET', path, headers = partial_headers )
|
||||
|
||||
response = connection.getresponse()
|
||||
|
||||
data = response.read()
|
||||
|
||||
self.assertEqual( response.status, 416 )
|
||||
|
||||
#
|
||||
|
||||
path = '/get_files/thumbnail?file_id={}'.format( 1 )
|
||||
|
|
|
@ -1211,7 +1211,7 @@ class TestClientDB( unittest.TestCase ):
|
|||
|
||||
result = self._read( 'serialisable_simple', 'pixiv_account' )
|
||||
|
||||
self.assertTrue( result, ( pixiv_id, password ) )
|
||||
self.assertEqual( result, [ pixiv_id, password ] )
|
||||
|
||||
|
||||
def test_services( self ):
|
||||
|
|
|
@ -641,8 +641,8 @@ class TestNetworkingJob( unittest.TestCase ):
|
|||
|
||||
tracker = bm.GetTracker( ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT )
|
||||
|
||||
self.assertTrue( tracker.GetUsage( HC.BANDWIDTH_TYPE_REQUESTS, None ), 1 )
|
||||
self.assertTrue( tracker.GetUsage( HC.BANDWIDTH_TYPE_DATA, None ), 256 )
|
||||
self.assertEqual( tracker.GetUsage( HC.BANDWIDTH_TYPE_REQUESTS, None ), 1 )
|
||||
self.assertEqual( tracker.GetUsage( HC.BANDWIDTH_TYPE_DATA, None ), 256 )
|
||||
|
||||
|
||||
|
||||
|
@ -682,7 +682,7 @@ class TestNetworkingJob( unittest.TestCase ):
|
|||
|
||||
self.assertEqual( type( job.GetErrorException() ), HydrusExceptions.ServerException )
|
||||
|
||||
self.assertTrue( job.GetErrorText(), BAD_RESPONSE )
|
||||
self.assertEqual( job.GetErrorText(), BAD_RESPONSE.decode( 'ascii' ) )
|
||||
|
||||
self.assertEqual( job.GetStatus(), ( '500 - Internal Server Error', 18, 18, None ) )
|
||||
|
||||
|
@ -837,7 +837,7 @@ class TestNetworkingJobHydrus( unittest.TestCase ):
|
|||
|
||||
self.assertEqual( type( job.GetErrorException() ), HydrusExceptions.ServerException )
|
||||
|
||||
self.assertTrue( job.GetErrorText(), BAD_RESPONSE )
|
||||
self.assertEqual( job.GetErrorText(), BAD_RESPONSE.decode( 'ascii' ) )
|
||||
|
||||
self.assertEqual( job.GetStatus(), ( '500 - Internal Server Error', 18, 18, None ) )
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import random
|
||||
import unittest
|
||||
|
||||
|
@ -281,6 +282,140 @@ class TestStringMatch( unittest.TestCase ):
|
|||
self.assertTrue( re_string_match.Matches( 'abc123' ) )
|
||||
|
||||
|
||||
class TestStringSlicer( unittest.TestCase ):
|
||||
|
||||
def test_basics( self ):
|
||||
|
||||
a = 'a ' + os.urandom( 8 ).hex()
|
||||
b = 'b ' + os.urandom( 8 ).hex()
|
||||
c = 'c ' + os.urandom( 8 ).hex()
|
||||
d = 'd ' + os.urandom( 8 ).hex()
|
||||
e = 'e ' + os.urandom( 8 ).hex()
|
||||
f = 'f ' + os.urandom( 8 ).hex()
|
||||
g = 'g ' + os.urandom( 8 ).hex()
|
||||
h = 'h ' + os.urandom( 8 ).hex()
|
||||
i = 'i ' + os.urandom( 8 ).hex()
|
||||
j = 'j ' + os.urandom( 8 ).hex()
|
||||
|
||||
test_list = [ a, b, c, d, e, f, g, h, i, j ]
|
||||
|
||||
#
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 0, index_end = 1 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ a ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 1st string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 3, index_end = 4 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ d ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 4th string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -3, index_end = -2 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ h ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 3rd from last string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -1 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ j ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the last string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 15, index_end = 16 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 16th string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -15, index_end = -14 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 15th from last string' )
|
||||
|
||||
#
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 0 )
|
||||
self.assertEqual( slicer.Slice( test_list ), test_list )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 1st string and onwards' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ d, e, f, g, h, i, j ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 4th string and onwards' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ h, i, j ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 3rd from last string and onwards' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 15 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 16th string and onwards' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -15 )
|
||||
self.assertEqual( slicer.Slice( test_list ), test_list )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 15th from last string and onwards' )
|
||||
|
||||
#
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_end = 0 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting nothing' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_end = 3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ a, b, c ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting up to and including the 3rd string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_end = -3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ a, b, c, d, e, f, g ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting up to and including the 4th from last string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_end = 15 )
|
||||
self.assertEqual( slicer.Slice( test_list ), test_list )
|
||||
self.assertEqual( slicer.ToString(), 'selecting up to and including the 15th string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_end = -15 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting up to and including the 16th from last string' )
|
||||
|
||||
#
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 0, index_end = 5 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ a, b, c, d, e ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 1st string up to and including the 5th string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 3, index_end = 5 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ d, e ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 4th string up to and including the 5th string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -5, index_end = -3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ f, g ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 5th from last string up to and including the 4th from last string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 3, index_end = -3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [ d, e, f, g ] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 4th string up to and including the 4th from last string' )
|
||||
|
||||
#
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 3, index_end = 3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting nothing' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 5, index_end = 3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting nothing' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -3, index_end = -3 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting nothing' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -3, index_end = -5 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting nothing' )
|
||||
|
||||
#
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = 15, index_end = 20 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 16th string up to and including the 20th string' )
|
||||
|
||||
slicer = ClientParsing.StringSlicer( index_start = -15, index_end = -12 )
|
||||
self.assertEqual( slicer.Slice( test_list ), [] )
|
||||
self.assertEqual( slicer.ToString(), 'selecting the 15th from last string up to and including the 13th from last string' )
|
||||
|
||||
|
||||
class TestStringSorter( unittest.TestCase ):
|
||||
|
||||
def test_basics( self ):
|
||||
|
@ -299,7 +434,7 @@ class TestStringSorter( unittest.TestCase ):
|
|||
|
||||
random.shuffle( test_list )
|
||||
|
||||
self.assertTrue( sorter.Sort( test_list ), correct )
|
||||
self.assertEqual( sorter.Sort( test_list ), correct )
|
||||
|
||||
|
||||
|
||||
|
@ -356,15 +491,15 @@ class TestStringSplitter( unittest.TestCase ):
|
|||
|
||||
splitter = ClientParsing.StringSplitter( separator = ', ' )
|
||||
|
||||
self.assertTrue( splitter.Split( '123' ), [ '123' ] )
|
||||
self.assertTrue( splitter.Split( '1,2,3' ), [ '1,2,3' ] )
|
||||
self.assertTrue( splitter.Split( '1, 2, 3' ), [ '1', '2', '3' ] )
|
||||
self.assertEqual( splitter.Split( '123' ), [ '123' ] )
|
||||
self.assertEqual( splitter.Split( '1,2,3' ), [ '1,2,3' ] )
|
||||
self.assertEqual( splitter.Split( '1, 2, 3' ), [ '1', '2', '3' ] )
|
||||
|
||||
splitter = ClientParsing.StringSplitter( separator = ', ', max_splits = 2 )
|
||||
|
||||
self.assertTrue( splitter.Split( '123' ), [ '123' ] )
|
||||
self.assertTrue( splitter.Split( '1,2,3' ), [ '1,2,3' ] )
|
||||
self.assertTrue( splitter.Split( '1, 2, 3, 4' ), [ '1', '2', '3,4' ] )
|
||||
self.assertEqual( splitter.Split( '123' ), [ '123' ] )
|
||||
self.assertEqual( splitter.Split( '1,2,3' ), [ '1,2,3' ] )
|
||||
self.assertEqual( splitter.Split( '1, 2, 3, 4' ), [ '1', '2', '3, 4' ] )
|
||||
|
||||
|
||||
class TestStringProcessor( unittest.TestCase ):
|
||||
|
|
|
@ -172,6 +172,8 @@ class TestServer( unittest.TestCase ):
|
|||
|
||||
self.assertEqual( response, EXAMPLE_FILE )
|
||||
|
||||
#
|
||||
|
||||
try: os.remove( path )
|
||||
except: pass
|
||||
|
||||
|
|
Loading…
Reference in New Issue