From c8773bad861adc0c1daea36e52ce0448190f9557 Mon Sep 17 00:00:00 2001 From: Hydrus Network Developer Date: Wed, 4 Apr 2018 20:22:26 -0500 Subject: [PATCH] Version 301 --- help/changelog.html | 27 + help/contact.html | 6 - help/getting_started_tags.html | 13 +- help/import_tag_options.png | Bin 0 -> 32153 bytes help/import_tag_options_collapsed.png | Bin 7501 -> 0 bytes help/import_tag_options_expanded.png | Bin 9666 -> 0 bytes include/ClientCaches.py | 86 +- include/ClientDB.py | 104 +- include/ClientDaemons.py | 10 + include/ClientData.py | 40 +- include/ClientDefaults.py | 114 ++ include/ClientGUI.py | 7 +- include/ClientGUIACDropdown.py | 15 + include/ClientGUICanvas.py | 21 +- include/ClientGUIDialogs.py | 7 +- include/ClientGUIListBoxes.py | 5 + include/ClientGUIManagement.py | 316 ++++-- include/ClientGUIMedia.py | 22 +- include/ClientGUIPages.py | 198 +++- include/ClientGUIParsing.py | 164 ++- include/ClientGUIScrolledPanelsReview.py | 19 +- include/ClientGUISeedCache.py | 2 +- include/ClientImporting.py | 1302 +++++++++++----------- include/ClientMedia.py | 128 +-- include/HydrusConstants.py | 2 +- include/HydrusGlobals.py | 1 + include/HydrusSerialisable.py | 2 +- include/TestDB.py | 4 +- 28 files changed, 1624 insertions(+), 991 deletions(-) create mode 100644 help/import_tag_options.png delete mode 100644 help/import_tag_options_collapsed.png delete mode 100644 help/import_tag_options_expanded.png diff --git a/help/changelog.html b/help/changelog.html index 0dde1a48..16b7fab1 100755 --- a/help/changelog.html +++ b/help/changelog.html @@ -8,6 +8,33 @@

changelog

\ No newline at end of file diff --git a/help/getting_started_tags.html b/help/getting_started_tags.html index e5220ebd..bbf3a3ba 100755 --- a/help/getting_started_tags.html +++ b/help/getting_started_tags.html @@ -28,6 +28,12 @@

If you add more tags or system predicates to a search, you will limit the results to those files that match every single one:

You can also exclude a tag by prefixing it with a hyphen (e.g. '-heresy').

+

importing tags from galleries

+

In several places around the hydrus client, always in the context of importing files from another location, you will see a tag import options button:

+

+

The namespaces listed are those that hydrus knows how to parse from where you are downloading from. Selecting one will tell hydrus to get those tags and set/pend them to the respective tag service.

+

'Explicit tags' are a way to force-add some additional tags for every file that comes through this import context. This can be useful, sometimes, to create personal 'processing' tags on your local tags like 'from tumblr' or 'imported on sunday' that you can revisit to find this download's files again.

+

You can quickly get thousands of tags in a few minutes this way!

tag repositories

It can take a long time to tag even this small number of files well, so I created tag repositories so people can share the work.

Tag repos store many file->tag relationships. Anyone who has an access key to the repository can sync with it and hence download all these relationships. If any of their own files match up, they will get those tags. Access keys will also usually have permission to upload new tags and ask for existing ones to be deleted.

@@ -47,13 +53,6 @@

Please do not spam tags to my public tag repo until you get a rough feel for the tag schema, or just lurk until you get the idea. I am not very strict about it (it is not difficult to correct mistakes), but I essentially only want factual tags—no subjective opinion.

You can connect to many different tag repositories, if you like. When you are in the manage tags dialog, pressing the up or down arrow keys on an empty input switches between your services.

FAQ: why can my friend not see what I just uploaded?

-

importing tags from galleries

-

In several places around the hydrus client, always in the context of importing files from another location, you will see this:

-

-

If you hit the 'expand' buttons, you will get some more options for the imports. The tag ones are like this:

-

-

The namespaces listed are those that hydrus knows how to parse from the gallery site or wherever you are downloading files from. Selecting one will tell hydrus to get those tags and set/pend them to the respective tag service.

-

You can quickly get thousands of tags in a few minutes this way!

Read about ratings --->

Go back to the index --->

diff --git a/help/import_tag_options.png b/help/import_tag_options.png new file mode 100644 index 0000000000000000000000000000000000000000..9f0f13a93fee4135416b07ed6205899c5205ea58 GIT binary patch literal 32153 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 DI(KK7 literal 0 HcmV?d00001 diff --git a/help/import_tag_options_collapsed.png b/help/import_tag_options_collapsed.png deleted file mode 100644 index 14016f5141296a0596955b920659a953de0d9c9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7501 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 G^!-0EJy|UP diff --git a/help/import_tag_options_expanded.png b/help/import_tag_options_expanded.png deleted file mode 100644 index 5a0f9c5d6624bd6dbab0e940023abc5cebdfb57b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9666 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 0: + if move_location is None and len( orphan_paths ) > 0: - if move_location is None: + status = 'found ' + HydrusData.ConvertIntToPrettyString( len( orphan_paths ) ) + ' orphans, now deleting' + + job_key.SetVariable( 'popup_text_1', status ) + + time.sleep( 5 ) + + for path in orphan_paths: - status = 'found ' + HydrusData.ConvertIntToPrettyString( len( orphan_paths ) ) + ' orphans, now deleting' + ( i_paused, should_quit ) = job_key.WaitIfNeeded() + + if should_quit: + + return + + + HydrusData.Print( 'Deleting the orphan ' + path ) + + status = 'deleting orphan files: ' + HydrusData.ConvertValueRangeToPrettyString( i + 1, len( orphan_paths ) ) job_key.SetVariable( 'popup_text_1', status ) - time.sleep( 5 ) - - for path in orphan_paths: - - ( i_paused, should_quit ) = job_key.WaitIfNeeded() - - if should_quit: - - return - - - HydrusData.Print( 'Deleting the orphan ' + path ) - - status = 'deleting orphan files: ' + HydrusData.ConvertValueRangeToPrettyString( i + 1, len( orphan_paths ) ) - - job_key.SetVariable( 'popup_text_1', status ) - - HydrusPaths.DeletePath( path ) - - - else: - - status = 'found ' + HydrusData.ConvertIntToPrettyString( len( orphan_paths ) ) + ' orphans, now moving to ' + move_location - - job_key.SetVariable( 'popup_text_1', status ) - - time.sleep( 5 ) - - for path in orphan_paths: - - ( i_paused, should_quit ) = job_key.WaitIfNeeded() - - if should_quit: - - return - - - ( source_dir, filename ) = os.path.split( path ) - - dest = os.path.join( move_location, filename ) - - dest = HydrusPaths.AppendPathUntilNoConflicts( dest ) - - HydrusData.Print( 'Moving the orphan ' + path + ' to ' + dest ) - - status = 'moving orphan files: ' + HydrusData.ConvertValueRangeToPrettyString( i + 1, len( orphan_paths ) ) - - job_key.SetVariable( 'popup_text_1', status ) - - HydrusPaths.MergeFile( path, dest ) - + HydrusPaths.DeletePath( path ) diff --git a/include/ClientDB.py b/include/ClientDB.py index 1d5f9f21..b573b525 100755 --- a/include/ClientDB.py +++ b/include/ClientDB.py @@ -4204,19 +4204,6 @@ class DB( HydrusDB.HydrusDB ): query_hash_ids = update_qhi( query_hash_ids, similar_hash_ids ) - if 'known_url_rules' in simple_preds: - - for ( operator, rule_type, rule ) in simple_preds[ 'known_url_rules' ]: - - if operator: # inclusive - - url_hash_ids = self._GetHashIdsFromURLRule( rule_type, rule ) - - query_hash_ids = update_qhi( query_hash_ids, url_hash_ids ) - - - - # now the simple preds and typical ways to populate query_hash_ids if 'min_size' in simple_preds: files_info_predicates.append( 'size > ' + str( simple_preds[ 'min_size' ] ) ) @@ -4463,19 +4450,6 @@ class DB( HydrusDB.HydrusDB ): # - if 'known_url_rules' in simple_preds: - - for ( operator, rule_type, rule ) in simple_preds[ 'known_url_rules' ]: - - if not operator: # exclusive - - url_hash_ids = self._GetHashIdsFromURLRule( rule_type, rule ) - - query_hash_ids.difference_update( url_hash_ids ) - - - - ( file_services_to_include_current, file_services_to_include_pending, file_services_to_exclude_current, file_services_to_exclude_pending ) = system_predicates.GetFileServiceInfo() for service_key in file_services_to_include_current: @@ -4619,6 +4593,25 @@ class DB( HydrusDB.HydrusDB ): query_hash_ids.difference_update( self._inbox_hash_ids ) + # + + if 'known_url_rules' in simple_preds: + + for ( operator, rule_type, rule ) in simple_preds[ 'known_url_rules' ]: + + url_hash_ids = self._GetHashIdsFromURLRule( rule_type, rule, hash_ids = query_hash_ids ) + + if operator: # inclusive + + query_hash_ids.intersection_update( url_hash_ids ) + + else: + + query_hash_ids.difference_update( url_hash_ids ) + + + + # num_tags_zero = False @@ -4903,29 +4896,38 @@ class DB( HydrusDB.HydrusDB ): return hash_ids - def _GetHashIdsFromURLRule( self, rule_type, rule ): + def _GetHashIdsFromURLRule( self, rule_type, rule, hash_ids = None ): - hash_ids = set() + if hash_ids is None: + + query = self._c.execute( 'SELECT hash_id, url FROM urls;' ) + + else: + + query = self._SelectFromList( 'SELECT hash_id, url FROM urls WHERE hash_id in %s;', hash_ids ) + - for ( hash_id, url ) in self._c.execute( 'SELECT hash_id, url FROM urls;' ): + result_hash_ids = set() + + for ( hash_id, url ) in query: if rule_type == 'url_match': if rule.Matches( url ): - hash_ids.add( hash_id ) + result_hash_ids.add( hash_id ) else: if re.search( rule, url ) is not None: - hash_ids.add( hash_id ) + result_hash_ids.add( hash_id ) - return hash_ids + return result_hash_ids def _GetHashIdsFromWildcard( self, file_service_key, tag_service_key, wildcard, include_current_tags, include_pending_tags ): @@ -6412,7 +6414,7 @@ class DB( HydrusDB.HydrusDB ): if service_key is None: - service_ids_to_statuses_and_pair_ids = HydrusData.BuildKeyToListDict( ( ( service_id, ( status, child_tag_id, parent_tag_id ) ) for ( service_id, child_tag_id, parent_tag_id, status ) in self._c.execute( 'SELECT service_id, status, child_tag_id, parent_tag_id FROM tag_parents UNION SELECT service_id, status, child_tag_id, parent_tag_id FROM tag_parent_petitions;' ) ) ) + service_ids_to_statuses_and_pair_ids = HydrusData.BuildKeyToListDict( ( ( service_id, ( status, child_tag_id, parent_tag_id ) ) for ( service_id, status, child_tag_id, parent_tag_id ) in self._c.execute( 'SELECT service_id, status, child_tag_id, parent_tag_id FROM tag_parents UNION SELECT service_id, status, child_tag_id, parent_tag_id FROM tag_parent_petitions;' ) ) ) service_keys_to_statuses_to_pairs = collections.defaultdict( HydrusData.default_dict_set ) @@ -9862,6 +9864,42 @@ class DB( HydrusDB.HydrusDB ): self._AddService( CC.LOCAL_NOTES_SERVICE_KEY, HC.LOCAL_NOTES, CC.LOCAL_NOTES_SERVICE_KEY, dictionary ) + if version == 300: + + try: + + sank_nc = ClientNetworking.NetworkContext( CC.NETWORK_CONTEXT_DOMAIN, 'sankakucomplex.com' ) + + bandwidth_manager = self._GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_BANDWIDTH_MANAGER ) + + rules = bandwidth_manager.GetRules( sank_nc ) + + rules = rules.Duplicate() + + rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 64 * 1024 * 1024 ) # added as a compromise to try to reduce hydrus sankaku bandwidth usage until their new API and subscription model comes in + + bandwidth_manager.SetRules( sank_nc, rules ) + + self._SetJSONDump( bandwidth_manager ) + + message = 'Sankaku Complex have mentioned to me, Hydrus Dev, that they have recently been running into bandwidth problems. They were respectful in reaching out to me and I am sympathetic to their problem. After some discussion, rather than removing hydrus support for Sankaku entirely, I am in this version adding a new restrictive default bandwidth rule for the sankakucomplex.com domain of 64MB/day.' + + self.pub_initial_message( message ) + + message = 'If you are a heavy Sankaku downloader, please bear with this limit until we can come up with a better solution. They told me they have plans for API upgrades and will be rolling out a subscription service in the coming months that may relieve this problem. I also expect to write some way to embed \'Here is how to support this source: (LINK)\' links into the downloader ui of my new downloader engine for those who can and wish to help out with bandwidth costs. Please check my release post if you would like to read more, and feel free to contact me directly to discuss it further.' + + self.pub_initial_message( message ) + + except Exception as e: + + HydrusData.PrintException( e ) + + message = 'Attempting to add a new rule to sankaku\'s domain failed. The error has been printed to your log file--please let hydrus dev know the details.' + + self.pub_initial_message( message ) + + + self._controller.pub( 'splash_set_title_text', 'updated db to v' + str( version + 1 ) ) self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) ) diff --git a/include/ClientDaemons.py b/include/ClientDaemons.py index 1e0edd85..3de1c365 100644 --- a/include/ClientDaemons.py +++ b/include/ClientDaemons.py @@ -317,6 +317,11 @@ def DAEMONSynchroniseRepositories( controller ): def DAEMONSynchroniseSubscriptions( controller ): + if HG.subscription_report_mode: + + HydrusData.ShowText( 'Subscription daemon started a run.' ) + + subscription_names = list( controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION ) ) if controller.new_options.GetBoolean( 'process_subs_in_random_order' ): @@ -337,6 +342,11 @@ def DAEMONSynchroniseSubscriptions( controller ): p1 = controller.options[ 'pause_subs_sync' ] p2 = HydrusThreading.IsThreadShuttingDown() + if HG.subscription_report_mode: + + HydrusData.ShowText( 'Subscription "' + name + '" about to start. Global sub pause is ' + str( p1 ) + ' and thread shutdown status is ' + str( p2 ) + '.' ) + + if p1 or p2: return diff --git a/include/ClientData.py b/include/ClientData.py index 0c86ea81..f3f56008 100644 --- a/include/ClientData.py +++ b/include/ClientData.py @@ -1142,6 +1142,10 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): # + self._dictionary[ 'simple_downloader_formulae' ] = HydrusSerialisable.SerialisableDictionary() + + # + self._dictionary[ 'noneable_strings' ] = {} self._dictionary[ 'noneable_strings' ][ 'favourite_file_lookup_script' ] = 'gelbooru md5' @@ -1157,6 +1161,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): self._dictionary[ 'strings' ][ 'namespace_connector' ] = ':' self._dictionary[ 'strings' ][ 'export_phrase' ] = '{hash}' self._dictionary[ 'strings' ][ 'current_colourset' ] = 'default' + self._dictionary[ 'strings' ][ 'favourite_simple_downloader_formula' ] = 'all images' self._dictionary[ 'string_list' ] = {} @@ -1626,11 +1631,15 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): guidance_tag_import_options = default_tag_import_options[ default_gallery_identifier ] + fetch_tags_even_if_url_known_and_file_already_in_db = False + service_keys_to_namespaces = {} service_keys_to_explicit_tags = {} if guidance_tag_import_options is not None: + fetch_tags_even_if_url_known_and_file_already_in_db = guidance_tag_import_options.ShouldFetchTagsEvenIfURLKnownAndFileAlreadyInDB() + ( namespaces, search_value ) = ClientDefaults.GetDefaultNamespacesAndSearchValue( gallery_identifier ) guidance_service_keys_to_namespaces = guidance_tag_import_options.GetServiceKeysToNamespaces() @@ -1652,7 +1661,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): import ClientImporting - tag_import_options = ClientImporting.TagImportOptions( service_keys_to_namespaces = service_keys_to_namespaces, service_keys_to_explicit_tags = service_keys_to_explicit_tags ) + tag_import_options = ClientImporting.TagImportOptions( fetch_tags_even_if_url_known_and_file_already_in_db = fetch_tags_even_if_url_known_and_file_already_in_db, service_keys_to_namespaces = service_keys_to_namespaces, service_keys_to_explicit_tags = service_keys_to_explicit_tags ) return tag_import_options @@ -1827,6 +1836,22 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): + def GetSimpleDownloaderFormulae( self ): + + with self._lock: + + if len( self._dictionary[ 'simple_downloader_formulae' ] ) == 0: + + for ( formula_name, formula ) in ClientDefaults.GetDefaultSimpleDownloaderFormulae(): + + self._dictionary[ 'simple_downloader_formulae' ][ formula_name ] = formula + + + + return self._dictionary[ 'simple_downloader_formulae' ].items() + + + def GetString( self, name ): with self._lock: @@ -2067,6 +2092,19 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): + def SetSimpleDownloaderFormulae( self, formulae ): + + with self._lock: + + self._dictionary[ 'simple_downloader_formulae' ] = HydrusSerialisable.SerialisableDictionary() + + for ( formula_name, formula ) in formulae: + + self._dictionary[ 'simple_downloader_formulae' ][ formula_name ] = formula + + + + def SetString( self, name, value ): with self._lock: diff --git a/include/ClientDefaults.py b/include/ClientDefaults.py index 9c234fa9..96aad8a6 100644 --- a/include/ClientDefaults.py +++ b/include/ClientDefaults.py @@ -92,6 +92,8 @@ def SetDefaultBandwidthManagerRules( bandwidth_manager ): rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 2 * GB ) # keep this in there so subs can know better when to stop running (the files come from a subdomain, which causes a pain for bandwidth calcs) + rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 64 * MB ) # added as a compromise to try to reduce hydrus sankaku bandwidth usage until their new API and subscription model comes in + bandwidth_manager.SetRules( ClientNetworking.NetworkContext( CC.NETWORK_CONTEXT_DOMAIN, 'sankakucomplex.com' ), rules ) def SetDefaultDomainManagerData( domain_manager ): @@ -809,6 +811,118 @@ def GetDefaultShortcuts(): return shortcuts +def GetDefaultSimpleDownloaderFormulae(): + + import ClientParsing + + formulae = [] + + # + + formula_name = 'all images' + + tag_rules = [ ( 'img', {}, None ) ] + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE + attribute_to_fetch = 'src' + + formula = ClientParsing.ParseFormulaHTML( tag_rules = tag_rules, content_to_fetch = content_to_fetch, attribute_to_fetch = attribute_to_fetch ) + + formulae.append( ( formula_name, formula ) ) + + # + + formula_name = '4chan thread (html)' + + tag_rules = [ ( 'div', { 'class' : 'fileText' }, None ), ( 'a', {}, 0 ) ] + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE + attribute_to_fetch = 'href' + + formula = ClientParsing.ParseFormulaHTML( tag_rules = tag_rules, content_to_fetch = content_to_fetch, attribute_to_fetch = attribute_to_fetch ) + + formulae.append( ( formula_name, formula ) ) + + # + + formula_name = '8chan thread (html)' + + tag_rules = [ ( 'p', { 'class' : 'fileinfo' }, None ), ( 'a', {}, 0 ) ] + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE + attribute_to_fetch = 'href' + + formula = ClientParsing.ParseFormulaHTML( tag_rules = tag_rules, content_to_fetch = content_to_fetch, attribute_to_fetch = attribute_to_fetch ) + + formulae.append( ( formula_name, formula ) ) + + # + + formula_name = 'twitter image' + + tag_rules = [ ( 'div', { 'class' : 'permalink-tweet' }, 0 ), ( 'div', { 'class' : 'AdaptiveMedia-container' }, None ), ( 'img', {}, None ) ] + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE + attribute_to_fetch = 'src' + + string_converter = ClientParsing.StringConverter( transformations = [ ( ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ':orig' ) ], example_string = 'https://pbs.twimg.com/media/DZoQ2SdXcAIrFkm.jpg' ) + + formula = ClientParsing.ParseFormulaHTML( tag_rules = tag_rules, content_to_fetch = content_to_fetch, attribute_to_fetch = attribute_to_fetch, string_converter = string_converter ) + + formulae.append( ( formula_name, formula ) ) + + # + + formula_name = 'gfycat webm' + + tag_rules = [ ( 'video', {}, None ), ( 'source', {}, None ) ] + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE + attribute_to_fetch = 'src' + + string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_REGEX, match_value = '\\.webm$', example_string = 'https://giant.gfycat.com/HatefulBleakBluegill.webm' ) + + formula = ClientParsing.ParseFormulaHTML( tag_rules = tag_rules, content_to_fetch = content_to_fetch, attribute_to_fetch = attribute_to_fetch, string_match = string_match ) + + formulae.append( ( formula_name, formula ) ) + + # + + formula_name = 'gfycat mp4' + + tag_rules = [ ( 'video', {}, None ), ( 'source', {}, None ) ] + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE + attribute_to_fetch = 'src' + + string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_REGEX, match_value = '\\.mp4$', example_string = 'https://giant.gfycat.com/HatefulBleakBluegill.mp4' ) + + formula = ClientParsing.ParseFormulaHTML( tag_rules = tag_rules, content_to_fetch = content_to_fetch, attribute_to_fetch = attribute_to_fetch, string_match = string_match ) + + formulae.append( ( formula_name, formula ) ) + + # + + formula_name = 'imgur video' + + tag_rules = [ ( 'meta', { 'property' : 'og:video' }, None ) ] + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE + attribute_to_fetch = 'content' + + formula = ClientParsing.ParseFormulaHTML( tag_rules = tag_rules, content_to_fetch = content_to_fetch, attribute_to_fetch = attribute_to_fetch ) + + formulae.append( ( formula_name, formula ) ) + + # + + formula_name = 'imgur image' + + tag_rules = [ ( 'link', { 'rel' : 'image_src' }, None ) ] + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE + attribute_to_fetch = 'href' + + formula = ClientParsing.ParseFormulaHTML( tag_rules = tag_rules, content_to_fetch = content_to_fetch, attribute_to_fetch = attribute_to_fetch ) + + formulae.append( ( formula_name, formula ) ) + + # + + return formulae + def GetDefaultURLMatches(): url_match_dir = os.path.join( HC.STATIC_DIR, 'default', 'url_classes' ) diff --git a/include/ClientGUI.py b/include/ClientGUI.py index 79855761..24a7d754 100755 --- a/include/ClientGUI.py +++ b/include/ClientGUI.py @@ -1367,7 +1367,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ): ClientGUIMenus.AppendMenuItem( self, download_menu, 'url download', 'Open a new tab to download some raw urls.', self._notebook.NewPageImportURLs, on_deepest_notebook = True ) ClientGUIMenus.AppendMenuItem( self, download_menu, 'thread watcher', 'Open a new tab to watch a thread.', self._notebook.NewPageImportThreadWatcher, on_deepest_notebook = True ) - ClientGUIMenus.AppendMenuItem( self, download_menu, 'webpage of images', 'Open a new tab to download files from generic galleries or threads.', self._notebook.NewPageImportPageOfImages, on_deepest_notebook = True ) + ClientGUIMenus.AppendMenuItem( self, download_menu, 'simple downloader', 'Open a new tab to download files from generic galleries or threads.', self._notebook.NewPageImportSimpleDownloader, on_deepest_notebook = True ) gallery_menu = wx.Menu() @@ -1819,6 +1819,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ): ClientGUIMenus.AppendMenuCheckItem( self, report_modes, 'hover window report mode', 'Have the hover windows report their show/hide logic.', HG.hover_window_report_mode, self._SwitchBoolean, 'hover_window_report_mode' ) ClientGUIMenus.AppendMenuCheckItem( self, report_modes, 'network report mode', 'Have the network engine report new jobs.', HG.network_report_mode, self._SwitchBoolean, 'network_report_mode' ) ClientGUIMenus.AppendMenuCheckItem( self, report_modes, 'shortcut report mode', 'Have the new shortcut system report what shortcuts it catches and whether it matches an action.', HG.shortcut_report_mode, self._SwitchBoolean, 'shortcut_report_mode' ) + ClientGUIMenus.AppendMenuCheckItem( self, report_modes, 'subscription report mode', 'Have the subscription system report what it is doing.', HG.subscription_report_mode, self._SwitchBoolean, 'subscription_report_mode' ) ClientGUIMenus.AppendMenu( debug, report_modes, 'report modes' ) @@ -3003,6 +3004,10 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p HG.shortcut_report_mode = not HG.shortcut_report_mode + elif name == 'subscription_report_mode': + + HG.subscription_report_mode = not HG.subscription_report_mode + elif name == 'pubsub_profile_mode': HG.pubsub_profile_mode = not HG.pubsub_profile_mode diff --git a/include/ClientGUIACDropdown.py b/include/ClientGUIACDropdown.py index 43407ac1..b7853bb3 100644 --- a/include/ClientGUIACDropdown.py +++ b/include/ClientGUIACDropdown.py @@ -1458,3 +1458,18 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ): + def RefreshFavouriteTags( self ): + + favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) ) + + favourite_tags.sort() + + predicates = [ ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, tag ) for tag in favourite_tags ] + + parents_manager = HG.client_controller.GetManager( 'tag_parents' ) + + predicates = parents_manager.ExpandPredicates( CC.COMBINED_TAG_SERVICE_KEY, predicates ) + + self._favourites_list.SetPredicates( predicates ) + + diff --git a/include/ClientGUICanvas.py b/include/ClientGUICanvas.py index cf045f29..846c4882 100755 --- a/include/ClientGUICanvas.py +++ b/include/ClientGUICanvas.py @@ -3520,8 +3520,14 @@ class CanvasFilterDuplicates( CanvasWithHovers ): ( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event ) - if modifier == wx.ACCEL_NORMAL and key in CC.DELETE_KEYS: self._Delete() - elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete() + if modifier == wx.ACCEL_NORMAL and key in CC.DELETE_KEYS: + + self._Delete() + + elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: + + self._Undelete() + else: CanvasWithHovers.EventCharHook( self, event ) @@ -4067,8 +4073,15 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ): if result == wx.ID_CANCEL: - if self._current_media in self._kept: self._kept.remove( self._current_media ) - if self._current_media in self._deleted: self._deleted.remove( self._current_media ) + if self._current_media in self._kept: + + self._kept.remove( self._current_media ) + + + if self._current_media in self._deleted: + + self._deleted.remove( self._current_media ) + return diff --git a/include/ClientGUIDialogs.py b/include/ClientGUIDialogs.py index 145722cf..578614ee 100755 --- a/include/ClientGUIDialogs.py +++ b/include/ClientGUIDialogs.py @@ -322,13 +322,14 @@ class DialogFinishFiltering( Dialog ): Dialog.__init__( self, parent, 'are you sure?', position = 'center' ) - self._commit = wx.Button( self, id = wx.ID_YES, label = 'commit' ) + self._commit = ClientGUICommon.BetterButton( self, 'commit', self.EndModal, wx.ID_YES ) self._commit.SetForegroundColour( ( 0, 128, 0 ) ) - self._forget = wx.Button( self, id = wx.ID_NO, label = 'forget' ) + self._forget = ClientGUICommon.BetterButton( self, 'forget', self.EndModal, wx.ID_NO ) self._forget.SetForegroundColour( ( 128, 0, 0 ) ) - self._back = wx.Button( self, id = wx.ID_CANCEL, label = 'back to filtering' ) + self._back = ClientGUICommon.BetterButton( self, 'back to filtering', self.EndModal, wx.ID_CANCEL ) + self._back.SetId( wx.ID_CANCEL ) hbox = wx.BoxSizer( wx.HORIZONTAL ) diff --git a/include/ClientGUIListBoxes.py b/include/ClientGUIListBoxes.py index 63b1dc36..45a5ebd3 100644 --- a/include/ClientGUIListBoxes.py +++ b/include/ClientGUIListBoxes.py @@ -188,6 +188,11 @@ class AddEditDeleteListBox( wx.Panel ): return datas + def GetValue( self ): + + return self.GetData() + + class QueueListBox( wx.Panel ): def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_callable = None, edit_callable = None ): diff --git a/include/ClientGUIManagement.py b/include/ClientGUIManagement.py index 4b72fe32..ac3d1dfb 100755 --- a/include/ClientGUIManagement.py +++ b/include/ClientGUIManagement.py @@ -18,12 +18,15 @@ import ClientGUIImport import ClientGUIListBoxes import ClientGUIMedia import ClientGUIMenus +import ClientGUIParsing +import ClientGUIScrolledPanels import ClientGUIScrolledPanelsEdit import ClientGUISeedCache import ClientGUITime import ClientGUITopLevelWindows import ClientImporting import ClientMedia +import ClientParsing import ClientRendering import ClientSearch import ClientThreading @@ -50,7 +53,7 @@ ID_TIMER_DUMP = wx.NewId() MANAGEMENT_TYPE_DUMPER = 0 MANAGEMENT_TYPE_IMPORT_GALLERY = 1 -MANAGEMENT_TYPE_IMPORT_PAGE_OF_IMAGES = 2 +MANAGEMENT_TYPE_IMPORT_SIMPLE_DOWNLOADER = 2 MANAGEMENT_TYPE_IMPORT_HDD = 3 MANAGEMENT_TYPE_IMPORT_THREAD_WATCHER = 4 MANAGEMENT_TYPE_PETITIONS = 5 @@ -97,13 +100,13 @@ def CreateManagementControllerImportGallery( gallery_identifier ): return management_controller -def CreateManagementControllerImportPageOfImages(): +def CreateManagementControllerImportSimpleDownloader(): - management_controller = CreateManagementController( 'page download', MANAGEMENT_TYPE_IMPORT_PAGE_OF_IMAGES ) + management_controller = CreateManagementController( 'simple downloader', MANAGEMENT_TYPE_IMPORT_SIMPLE_DOWNLOADER ) - page_of_images_import = ClientImporting.PageOfImagesImport() + simple_downloader_import = ClientImporting.SimpleDownloaderImport() - management_controller.SetVariable( 'page_of_images_import', page_of_images_import ) + management_controller.SetVariable( 'simple_downloader_import', simple_downloader_import ) return management_controller @@ -538,7 +541,7 @@ class ManagementController( HydrusSerialisable.SerialisableBase ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_MANAGEMENT_CONTROLLER SERIALISABLE_NAME = 'Client Page Management Controller' - SERIALISABLE_VERSION = 3 + SERIALISABLE_VERSION = 4 def __init__( self, page_name = 'page' ): @@ -576,7 +579,7 @@ class ManagementController( HydrusSerialisable.SerialisableBase ): def _InitialiseFromSerialisableInfo( self, serialisable_info ): - ( self._page_name, self._management_type, serialisable_keys, serialisable_simples, serialisables ) = serialisable_info + ( self._page_name, self._management_type, serialisable_keys, serialisable_simples, serialisable_serialisables ) = serialisable_info self._InitialiseDefaults() @@ -592,7 +595,7 @@ class ManagementController( HydrusSerialisable.SerialisableBase ): self._simples.update( dict( serialisable_simples ) ) - self._serialisables.update( { name : HydrusSerialisable.CreateFromSerialisableTuple( value ) for ( name, value ) in serialisables.items() } ) + self._serialisables.update( { name : HydrusSerialisable.CreateFromSerialisableTuple( value ) for ( name, value ) in serialisable_serialisables.items() } ) def _UpdateSerialisableInfo( self, version, old_serialisable_info ): @@ -653,6 +656,22 @@ class ManagementController( HydrusSerialisable.SerialisableBase ): return ( 3, new_serialisable_info ) + if version == 3: + + ( page_name, management_type, serialisable_keys, serialisable_simples, serialisable_serialisables ) = old_serialisable_info + + if 'page_of_images_import' in serialisable_serialisables: + + serialisable_serialisables[ 'simple_downloader_import' ] = serialisable_serialisables[ 'page_of_images_import' ] + + del serialisable_serialisables[ 'page_of_images_import' ] + + + new_serialisable_info = ( page_name, management_type, serialisable_keys, serialisable_simples, serialisable_serialisables ) + + return ( 4, new_serialisable_info ) + + def GetKey( self, name ): @@ -686,9 +705,19 @@ class ManagementController( HydrusSerialisable.SerialisableBase ): return name in self._simples or name in self._serialisables + def IsDeadThreadWatcher( self ): + + if self._management_type == MANAGEMENT_TYPE_IMPORT_THREAD_WATCHER: + + thread_watcher_import = self.GetVariable( 'thread_watcher_import' ) + + return thread_watcher_import.IsDead() + + + def IsImporter( self ): - return self._management_type in ( MANAGEMENT_TYPE_IMPORT_GALLERY, MANAGEMENT_TYPE_IMPORT_HDD, MANAGEMENT_TYPE_IMPORT_PAGE_OF_IMAGES, MANAGEMENT_TYPE_IMPORT_THREAD_WATCHER, MANAGEMENT_TYPE_IMPORT_URLS ) + return self._management_type in ( MANAGEMENT_TYPE_IMPORT_GALLERY, MANAGEMENT_TYPE_IMPORT_HDD, MANAGEMENT_TYPE_IMPORT_SIMPLE_DOWNLOADER, MANAGEMENT_TYPE_IMPORT_THREAD_WATCHER, MANAGEMENT_TYPE_IMPORT_URLS ) def SetKey( self, name, key ): @@ -1742,17 +1771,17 @@ class ManagementPanelImporterHDD( ManagementPanelImporter ): management_panel_types_to_classes[ MANAGEMENT_TYPE_IMPORT_HDD ] = ManagementPanelImporterHDD -class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): +class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ): def __init__( self, parent, page, controller, management_controller ): ManagementPanelImporter.__init__( self, parent, page, controller, management_controller ) - self._page_of_images_panel = ClientGUICommon.StaticBox( self, 'page of images downloader' ) + self._simple_downloader_panel = ClientGUICommon.StaticBox( self, 'simple downloader' ) # - self._import_queue_panel = ClientGUICommon.StaticBox( self._page_of_images_panel, 'imports' ) + self._import_queue_panel = ClientGUICommon.StaticBox( self._simple_downloader_panel, 'imports' ) self._pause_files_button = wx.BitmapButton( self._import_queue_panel, bitmap = CC.GlobalBMPs.pause ) self._pause_files_button.Bind( wx.EVT_BUTTON, self.EventPauseFiles ) @@ -1763,41 +1792,43 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): # - self._pending_page_urls_panel = ClientGUICommon.StaticBox( self._page_of_images_panel, 'pending page urls' ) + self._pending_jobs_panel = ClientGUICommon.StaticBox( self._simple_downloader_panel, 'pending urls' ) - self._pause_queue_button = wx.BitmapButton( self._pending_page_urls_panel, bitmap = CC.GlobalBMPs.pause ) + self._pause_queue_button = wx.BitmapButton( self._pending_jobs_panel, bitmap = CC.GlobalBMPs.pause ) self._pause_queue_button.Bind( wx.EVT_BUTTON, self.EventPauseQueue ) - self._parser_status = ClientGUICommon.BetterStaticText( self._pending_page_urls_panel ) + self._parser_status = ClientGUICommon.BetterStaticText( self._pending_jobs_panel ) - self._page_download_control = ClientGUIControls.NetworkJobControl( self._pending_page_urls_panel ) + self._page_download_control = ClientGUIControls.NetworkJobControl( self._pending_jobs_panel ) - self._pending_page_urls_listbox = wx.ListBox( self._pending_page_urls_panel, size = ( -1, 100 ) ) + self._pending_jobs_listbox = wx.ListBox( self._pending_jobs_panel, size = ( -1, 100 ) ) - self._advance_button = wx.Button( self._pending_page_urls_panel, label = u'\u2191' ) + self._advance_button = wx.Button( self._pending_jobs_panel, label = u'\u2191' ) self._advance_button.Bind( wx.EVT_BUTTON, self.EventAdvance ) - self._delete_button = wx.Button( self._pending_page_urls_panel, label = 'X' ) + self._delete_button = wx.Button( self._pending_jobs_panel, label = 'X' ) self._delete_button.Bind( wx.EVT_BUTTON, self.EventDelete ) - self._delay_button = wx.Button( self._pending_page_urls_panel, label = u'\u2193' ) + self._delay_button = wx.Button( self._pending_jobs_panel, label = u'\u2193' ) self._delay_button.Bind( wx.EVT_BUTTON, self.EventDelay ) - self._page_url_input = ClientGUICommon.TextAndPasteCtrl( self._pending_page_urls_panel, self._PendPageURLs ) + self._page_url_input = ClientGUICommon.TextAndPasteCtrl( self._pending_jobs_panel, self._PendPageURLs ) - self._download_image_links = wx.CheckBox( self._page_of_images_panel, label = 'download image links' ) - self._download_image_links.Bind( wx.EVT_CHECKBOX, self.EventDownloadImageLinks ) - self._download_image_links.SetToolTip( 'i.e. download the href url of an tag if there is an tag nested beneath it' ) + self._formulae = ClientGUICommon.BetterChoice( self._pending_jobs_panel ) - self._download_unlinked_images = wx.CheckBox( self._page_of_images_panel, label = 'download unlinked images' ) - self._download_unlinked_images.Bind( wx.EVT_CHECKBOX, self.EventDownloadUnlinkedImages ) - self._download_unlinked_images.SetToolTip( 'i.e. download the src url of an tag if there is no parent tag' ) + menu_items = [] - self._page_of_images_import = self._management_controller.GetVariable( 'page_of_images_import' ) + menu_items.append( ( 'normal', 'edit formulae', 'Edit these parsing formulae.', self._EditFormulae ) ) - ( file_import_options, download_image_links, download_unlinked_images ) = self._page_of_images_import.GetOptions() + self._formula_cog = ClientGUICommon.MenuBitmapButton( self._pending_jobs_panel, CC.GlobalBMPs.cog, menu_items ) - self._file_import_options = ClientGUIImport.FileImportOptionsButton( self._page_of_images_panel, file_import_options, self._page_of_images_import.SetFileImportOptions ) + self._RefreshFormulae() + + self._simple_downloader_import = self._management_controller.GetVariable( 'simple_downloader_import' ) + + file_import_options = self._simple_downloader_import.GetFileImportOptions() + + self._file_import_options = ClientGUIImport.FileImportOptionsButton( self._simple_downloader_panel, file_import_options, self._simple_downloader_import.SetFileImportOptions ) # @@ -1814,22 +1845,26 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): queue_hbox = wx.BoxSizer( wx.HORIZONTAL ) - queue_hbox.Add( self._pending_page_urls_listbox, CC.FLAGS_EXPAND_BOTH_WAYS ) + queue_hbox.Add( self._pending_jobs_listbox, CC.FLAGS_EXPAND_BOTH_WAYS ) queue_hbox.Add( queue_buttons_vbox, CC.FLAGS_VCENTER ) - self._pending_page_urls_panel.Add( self._parser_status, CC.FLAGS_EXPAND_PERPENDICULAR ) - self._pending_page_urls_panel.Add( self._page_download_control, CC.FLAGS_EXPAND_PERPENDICULAR ) - self._pending_page_urls_panel.Add( queue_hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) - self._pending_page_urls_panel.Add( self._page_url_input, CC.FLAGS_EXPAND_PERPENDICULAR ) - self._pending_page_urls_panel.Add( self._pause_queue_button, CC.FLAGS_LONE_BUTTON ) + formulae_hbox = wx.BoxSizer( wx.HORIZONTAL ) + + formulae_hbox.Add( self._formulae, CC.FLAGS_EXPAND_BOTH_WAYS ) + formulae_hbox.Add( self._formula_cog, CC.FLAGS_VCENTER ) + + self._pending_jobs_panel.Add( self._parser_status, CC.FLAGS_EXPAND_PERPENDICULAR ) + self._pending_jobs_panel.Add( self._page_download_control, CC.FLAGS_EXPAND_PERPENDICULAR ) + self._pending_jobs_panel.Add( queue_hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) + self._pending_jobs_panel.Add( self._page_url_input, CC.FLAGS_EXPAND_PERPENDICULAR ) + self._pending_jobs_panel.Add( formulae_hbox, CC.FLAGS_EXPAND_PERPENDICULAR ) + self._pending_jobs_panel.Add( self._pause_queue_button, CC.FLAGS_LONE_BUTTON ) # - self._page_of_images_panel.Add( self._import_queue_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) - self._page_of_images_panel.Add( self._pending_page_urls_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) - self._page_of_images_panel.Add( self._download_image_links, CC.FLAGS_EXPAND_PERPENDICULAR ) - self._page_of_images_panel.Add( self._download_unlinked_images, CC.FLAGS_EXPAND_PERPENDICULAR ) - self._page_of_images_panel.Add( self._file_import_options, CC.FLAGS_EXPAND_PERPENDICULAR ) + self._simple_downloader_panel.Add( self._import_queue_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) + self._simple_downloader_panel.Add( self._pending_jobs_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) + self._simple_downloader_panel.Add( self._file_import_options, CC.FLAGS_EXPAND_PERPENDICULAR ) # @@ -1839,7 +1874,7 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): self._collect_by.Hide() - vbox.Add( self._page_of_images_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) + vbox.Add( self._simple_downloader_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) self._MakeCurrentSelectionTagsBox( vbox ) @@ -1847,34 +1882,154 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): # - seed_cache = self._page_of_images_import.GetSeedCache() + seed_cache = self._simple_downloader_import.GetSeedCache() self._seed_cache_control.SetSeedCache( seed_cache ) - self._page_of_images_import.SetDownloadControlFile( self._file_download_control ) - self._page_of_images_import.SetDownloadControlPage( self._page_download_control ) - - self._download_image_links.SetValue( download_image_links ) - self._download_unlinked_images.SetValue( download_unlinked_images ) + self._simple_downloader_import.SetDownloadControlFile( self._file_download_control ) + self._simple_downloader_import.SetDownloadControlPage( self._page_download_control ) self._UpdateStatus() + def _EditFormulae( self ): + + def data_to_pretty_callable( data ): + + ( formula_name, formula ) = data + + return formula_name + + + def edit_callable( data ): + + ( formula_name, formula ) = data + + with ClientGUIDialogs.DialogTextEntry( dlg, 'edit name', default = formula_name ) as dlg_2: + + if dlg_2.ShowModal() == wx.ID_OK: + + formula_name = dlg_2.GetValue() + + else: + + return ( False, None ) + + + + with ClientGUITopLevelWindows.DialogEdit( dlg, 'edit formula' ) as dlg_3: + + panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg_3 ) + + control = ClientGUIParsing.EditFormulaPanel( panel, formula, lambda: ( {}, '' ) ) + + panel.SetControl( control ) + + dlg_3.SetPanel( panel ) + + if dlg_3.ShowModal() == wx.ID_OK: + + formula = control.GetValue() + + data = ( formula_name, formula ) + + return ( True, data ) + + else: + + return ( False, None ) + + + + + def add_callable(): + + formula_name = 'new formula' + + formula = ClientParsing.ParseFormulaHTML() + + data = ( formula_name, formula ) + + return edit_callable( data ) + + + formulae = list( self._controller.new_options.GetSimpleDownloaderFormulae() ) + + formulae.sort() + + with ClientGUITopLevelWindows.DialogEdit( self, 'edit simple downloader formulae' ) as dlg: + + panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg ) + + height_num_chars = 20 + + control = ClientGUIListBoxes.AddEditDeleteListBox( panel, height_num_chars, data_to_pretty_callable, add_callable, edit_callable ) + + control.AddDatas( formulae ) + + panel.SetControl( control ) + + dlg.SetPanel( panel ) + + if dlg.ShowModal() == wx.ID_OK: + + formulae = control.GetData() + + self._controller.new_options.SetSimpleDownloaderFormulae( formulae ) + + + + self._RefreshFormulae() + + def _PendPageURLs( self, urls ): urls = [ url for url in urls if url.startswith( 'http' ) ] + ( formula_name, formula ) = self._formulae.GetChoice() + + self._controller.new_options.SetString( 'favourite_simple_downloader_formula', formula_name ) + for url in urls: - self._page_of_images_import.PendPageURL( url ) + job = ( url, formula_name, formula ) + + self._simple_downloader_import.PendJob( job ) self._UpdateStatus() + def _RefreshFormulae( self ): + + self._formulae.Clear() + + favourite = None + favourite_name = self._controller.new_options.GetString( 'favourite_simple_downloader_formula' ) + + formulae = list( self._controller.new_options.GetSimpleDownloaderFormulae() ) + + formulae.sort() + + for ( i, ( formula_name, formula ) ) in enumerate( formulae ): + + self._formulae.Append( formula_name, ( formula_name, formula ) ) + + if formula_name == favourite_name: + + favourite = i + + + + if favourite is not None: + + self._formulae.Select( favourite ) + + + def _SeedCache( self ): - seed_cache = self._page_of_images_import.GetSeedCache() + seed_cache = self._simple_downloader_import.GetSeedCache() title = 'file import status' frame_key = 'file_import_status' @@ -1888,19 +2043,30 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): def _UpdateStatus( self ): - ( pending_page_urls, parser_status, current_action, queue_paused, files_paused ) = self._page_of_images_import.GetStatus() + ( pending_jobs, parser_status, current_action, queue_paused, files_paused ) = self._simple_downloader_import.GetStatus() - if self._pending_page_urls_listbox.GetStrings() != pending_page_urls: + current_pending_jobs = [ self._pending_jobs_listbox.GetClientData( i ) for i in range( self._pending_jobs_listbox.GetCount() ) ] + + if current_pending_jobs != pending_jobs: - selected_string = self._pending_page_urls_listbox.GetStringSelection() + selected_string = self._pending_jobs_listbox.GetStringSelection() - self._pending_page_urls_listbox.SetItems( pending_page_urls ) + self._pending_jobs_listbox.Clear() - selection_index = self._pending_page_urls_listbox.FindString( selected_string ) + for job in pending_jobs: + + ( url, formula_name, formula ) = job + + pretty_job = formula_name + ': ' + url + + self._pending_jobs_listbox.Append( pretty_job, job ) + + + selection_index = self._pending_jobs_listbox.FindString( selected_string ) if selection_index != wx.NOT_FOUND: - self._pending_page_urls_listbox.Select( selection_index ) + self._pending_jobs_listbox.Select( selection_index ) @@ -1939,13 +2105,13 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): def EventAdvance( self, event ): - selection = self._pending_page_urls_listbox.GetSelection() + selection = self._pending_jobs_listbox.GetSelection() if selection != wx.NOT_FOUND: - page_url = self._pending_page_urls_listbox.GetString( selection ) + job = self._pending_jobs_listbox.GetClientData( selection ) - self._page_of_images_import.AdvancePageURL( page_url ) + self._simple_downloader_import.AdvanceJob( job ) self._UpdateStatus() @@ -1953,13 +2119,13 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): def EventDelay( self, event ): - selection = self._pending_page_urls_listbox.GetSelection() + selection = self._pending_jobs_listbox.GetSelection() if selection != wx.NOT_FOUND: - page_url = self._pending_page_urls_listbox.GetString( selection ) + job = self._pending_jobs_listbox.GetClientData( selection ) - self._page_of_images_import.DelayPageURL( page_url ) + self._simple_downloader_import.DelayJob( job ) self._UpdateStatus() @@ -1967,38 +2133,28 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): def EventDelete( self, event ): - selection = self._pending_page_urls_listbox.GetSelection() + selection = self._pending_jobs_listbox.GetSelection() if selection != wx.NOT_FOUND: - page_url = self._pending_page_urls_listbox.GetString( selection ) + job = self._pending_jobs_listbox.GetClientData( selection ) - self._page_of_images_import.DeletePageURL( page_url ) + self._simple_downloader_import.DeleteJob( job ) self._UpdateStatus() - def EventDownloadImageLinks( self, event ): - - self._page_of_images_import.SetDownloadImageLinks( self._download_image_links.GetValue() ) - - - def EventDownloadUnlinkedImages( self, event ): - - self._page_of_images_import.SetDownloadUnlinkedImages( self._download_unlinked_images.GetValue() ) - - def EventPauseQueue( self, event ): - self._page_of_images_import.PausePlayQueue() + self._simple_downloader_import.PausePlayQueue() self._UpdateStatus() def EventPauseFiles( self, event ): - self._page_of_images_import.PausePlayFiles() + self._simple_downloader_import.PausePlayFiles() self._UpdateStatus() @@ -2015,12 +2171,12 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): def Start( self ): - self._page_of_images_import.Start( self._page_key ) + self._simple_downloader_import.Start( self._page_key ) def TestAbleToClose( self ): - if self._page_of_images_import.CurrentlyWorking(): + if self._simple_downloader_import.CurrentlyWorking(): with ClientGUIDialogs.DialogYesNo( self, 'This page is still importing. Are you sure you want to close it?' ) as dlg: @@ -2032,7 +2188,7 @@ class ManagementPanelImporterPageOfImages( ManagementPanelImporter ): -management_panel_types_to_classes[ MANAGEMENT_TYPE_IMPORT_PAGE_OF_IMAGES ] = ManagementPanelImporterPageOfImages +management_panel_types_to_classes[ MANAGEMENT_TYPE_IMPORT_SIMPLE_DOWNLOADER ] = ManagementPanelImporterSimpleDownloader class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ): diff --git a/include/ClientGUIMedia.py b/include/ClientGUIMedia.py index ecd94252..fe7c6a3c 100755 --- a/include/ClientGUIMedia.py +++ b/include/ClientGUIMedia.py @@ -1503,7 +1503,16 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ): self._focussed_media = media - HG.client_controller.pub( 'preview_changed', self._page_key, media ) + if self._focussed_media is None: + + publish_media = None + + else: + + publish_media = self._focussed_media.GetDisplayMedia() + + + HG.client_controller.pub( 'preview_changed', self._page_key, publish_media ) def _ShareOnLocalBooru( self ): @@ -1680,7 +1689,16 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ): def PageShown( self ): - HG.client_controller.pub( 'preview_changed', self._page_key, self._focussed_media ) + if self._focussed_media is None: + + publish_media = None + + else: + + publish_media = self._focussed_media.GetDisplayMedia() + + + HG.client_controller.pub( 'preview_changed', self._page_key, publish_media ) self._PublishSelectionChange() diff --git a/include/ClientGUIPages.py b/include/ClientGUIPages.py index 26f8cabe..97fbbd50 100755 --- a/include/ClientGUIPages.py +++ b/include/ClientGUIPages.py @@ -117,9 +117,9 @@ class DialogPageChooser( ClientGUIDialogs.Dialog ): button.SetLabelText( text ) - elif entry_type == 'page_import_page_of_images': + elif entry_type == 'page_import_simple_downloader': - button.SetLabelText( 'page of images' ) + button.SetLabelText( 'simple downloader' ) elif entry_type == 'page_import_thread_watcher': @@ -200,9 +200,9 @@ class DialogPageChooser( ClientGUIDialogs.Dialog ): self._result = ( 'page', ClientGUIManagement.CreateManagementControllerImportGallery( gallery_identifier ) ) - elif entry_type == 'page_import_page_of_images': + elif entry_type == 'page_import_simple_downloader': - self._result = ( 'page', ClientGUIManagement.CreateManagementControllerImportPageOfImages() ) + self._result = ( 'page', ClientGUIManagement.CreateManagementControllerImportSimpleDownloader() ) elif entry_type == 'page_import_thread_watcher': @@ -261,7 +261,7 @@ class DialogPageChooser( ClientGUIDialogs.Dialog ): entries.append( ( 'page_import_urls', None ) ) entries.append( ( 'page_import_thread_watcher', None ) ) entries.append( ( 'menu', 'gallery' ) ) - entries.append( ( 'page_import_page_of_images', None ) ) + entries.append( ( 'page_import_simple_downloader', None ) ) elif menu_keyword == 'gallery': @@ -963,6 +963,15 @@ class PagesNotebook( wx.Notebook ): + def _GatherDeadThreadWatchers( self, insertion_page ): + + top_notebook = self._GetTopNotebook() + + gathered_pages = top_notebook.GetGatherPages( 'dead_thread_watchers' ) + + self._MovePages( gathered_pages, insertion_page ) + + def _GetDefaultPageInsertionIndex( self ): new_options = self._controller.new_options @@ -1118,6 +1127,22 @@ class PagesNotebook( wx.Notebook ): return None + def _GetTopNotebook( self ): + + top_notebook = self + + parent = top_notebook.GetParent() + + while isinstance( parent, PagesNotebook ): + + top_notebook = parent + + parent = top_notebook.GetParent() + + + return top_notebook + + def _MovePage( self, page, dest_notebook, insertion_tab_index, follow_dropped_page = False ): source_notebook = page.GetParent() @@ -1151,6 +1176,21 @@ class PagesNotebook( wx.Notebook ): self._controller.pub( 'refresh_page_name', page.GetPageKey() ) + def _MovePages( self, pages, dest_notebook ): + + insertion_tab_index = dest_notebook.GetNumPages( only_my_level = True ) + + for page in pages: + + if page.GetParent() != dest_notebook: + + self._MovePage( page, dest_notebook, insertion_tab_index ) + + insertion_tab_index += 1 + + + + def _ShiftPage( self, page_index, delta = None, new_index = None ): new_page_index = page_index @@ -1289,13 +1329,16 @@ class PagesNotebook( wx.Notebook ): num_pages = self.GetPageCount() + end_index = num_pages - 1 + more_than_one_tab = num_pages > 1 click_over_tab = tab_index != -1 - click_over_page_of_pages = False + can_go_left = tab_index > 0 + can_go_right = tab_index < end_index - end_index = num_pages - 1 + click_over_page_of_pages = False existing_session_names = self._controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_GUI_SESSION ) @@ -1309,9 +1352,6 @@ class PagesNotebook( wx.Notebook ): ClientGUIMenus.AppendMenuItem( self, menu, 'close page', 'Close this page.', self._ClosePage, tab_index ) - can_go_left = tab_index > 0 - can_go_right = tab_index < end_index - if num_pages > 1: ClientGUIMenus.AppendMenuItem( self, menu, 'close other pages', 'Close all pages but this one.', self._CloseOtherPages, tab_index ) @@ -1327,15 +1367,6 @@ class PagesNotebook( wx.Notebook ): - ClientGUIMenus.AppendSeparator( menu ) - - ClientGUIMenus.AppendMenuItem( self, menu, 'send this page down to a new page of pages', 'Make a new page of pages and put this page in it.', self._SendPageToNewNotebook, tab_index ) - - if can_go_right: - - ClientGUIMenus.AppendMenuItem( self, menu, 'send pages to the right to a new page of pages', 'Make a new page of pages and put all the pages to the right into it.', self._SendRightPagesToNewNotebook, tab_index ) - - ClientGUIMenus.AppendSeparator( menu ) ClientGUIMenus.AppendMenuItem( self, menu, 'rename page', 'Rename this page.', self._RenamePage, tab_index ) @@ -1343,43 +1374,9 @@ class PagesNotebook( wx.Notebook ): ClientGUIMenus.AppendMenuItem( self, menu, 'new page', 'Choose a new page.', self._ChooseNewPage ) - if click_over_page_of_pages or len( existing_session_names ) > 0: - - ClientGUIMenus.AppendSeparator( menu ) - - - if len( existing_session_names ) > 0: - - submenu = wx.Menu() - - for name in existing_session_names: - - ClientGUIMenus.AppendMenuItem( self, submenu, name, 'Load this session here.', self.AppendGUISession, name ) - - - ClientGUIMenus.AppendMenu( menu, submenu, 'append session' ) - - if click_over_tab: - if click_over_page_of_pages: - - submenu = wx.Menu() - - for name in existing_session_names: - - if name == 'last session': - - continue - - - ClientGUIMenus.AppendMenuItem( self, submenu, name, 'Save this page of pages to the session.', page.SaveGUISession, name ) - - - ClientGUIMenus.AppendMenuItem( self, submenu, 'create a new session', 'Save this page of pages to the session.', page.SaveGUISession, suggested_name = page.GetDisplayName() ) - - ClientGUIMenus.AppendMenu( menu, submenu, 'save this page of pages to a session' ) - + ClientGUIMenus.AppendMenuItem( self, menu, 'new page here', 'Choose a new page.', self._ChooseNewPage, tab_index ) if more_than_one_tab: @@ -1390,10 +1387,6 @@ class PagesNotebook( wx.Notebook ): can_move_right = tab_index < end_index can_end = tab_index < end_index - 1 - ClientGUIMenus.AppendMenuItem( self, menu, 'new page here', 'Choose a new page.', self._ChooseNewPage, tab_index ) - - ClientGUIMenus.AppendSeparator( menu ) - if can_home: ClientGUIMenus.AppendMenuItem( self, menu, 'move to left end', 'Move this page all the way to the left.', self._ShiftPage, tab_index, new_index = 0 ) @@ -1415,6 +1408,15 @@ class PagesNotebook( wx.Notebook ): + ClientGUIMenus.AppendSeparator( menu ) + + ClientGUIMenus.AppendMenuItem( self, menu, 'send this page down to a new page of pages', 'Make a new page of pages and put this page in it.', self._SendPageToNewNotebook, tab_index ) + + if can_go_right: + + ClientGUIMenus.AppendMenuItem( self, menu, 'send pages to the right to a new page of pages', 'Make a new page of pages and put all the pages to the right into it.', self._SendRightPagesToNewNotebook, tab_index ) + + if click_over_page_of_pages and page.GetPageCount() > 0: ClientGUIMenus.AppendSeparator( menu ) @@ -1423,6 +1425,53 @@ class PagesNotebook( wx.Notebook ): + if click_over_page_of_pages: + + ClientGUIMenus.AppendSeparator( menu ) + + submenu = wx.Menu() + + ClientGUIMenus.AppendMenuItem( self, submenu, 'dead thread watchers', 'Find all currently open dead thread watchers and move them to this page of pages.', self._GatherDeadThreadWatchers, page ) + + ClientGUIMenus.AppendMenu( menu, submenu, 'gather on this page of pages' ) + + + if len( existing_session_names ) > 0 or click_over_page_of_pages: + + ClientGUIMenus.AppendSeparator( menu ) + + + if len( existing_session_names ) > 0: + + submenu = wx.Menu() + + for name in existing_session_names: + + ClientGUIMenus.AppendMenuItem( self, submenu, name, 'Load this session here.', self.AppendGUISession, name ) + + + ClientGUIMenus.AppendMenu( menu, submenu, 'append session' ) + + + if click_over_page_of_pages: + + submenu = wx.Menu() + + for name in existing_session_names: + + if name == 'last session': + + continue + + + ClientGUIMenus.AppendMenuItem( self, submenu, name, 'Save this page of pages to the session.', page.SaveGUISession, name ) + + + ClientGUIMenus.AppendMenuItem( self, submenu, 'create a new session', 'Save this page of pages to the session.', page.SaveGUISession, suggested_name = page.GetDisplayName() ) + + ClientGUIMenus.AppendMenu( menu, submenu, 'save this page of pages to a session' ) + + self._controller.PopupMenu( self, menu ) @@ -1753,6 +1802,35 @@ class PagesNotebook( wx.Notebook ): + def GetGatherPages( self, gather_type ): + + if gather_type == 'dead_thread_watchers': + + def test( page ): + + management_controller = page.GetManagementController() + + return management_controller.IsDeadThreadWatcher() + + + else: + + raise NotImplementedError() + + + gathered_pages = [] + + for page in self.GetMediaPages(): + + if test( page ): + + gathered_pages.append( page ) + + + + return gathered_pages + + def GetMediaPages( self, only_my_level = False ): return self._GetMediaPages( only_my_level ) @@ -2111,9 +2189,9 @@ class PagesNotebook( wx.Notebook ): return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook ) - def NewPageImportPageOfImages( self, on_deepest_notebook = False ): + def NewPageImportSimpleDownloader( self, on_deepest_notebook = False ): - management_controller = ClientGUIManagement.CreateManagementControllerImportPageOfImages() + management_controller = ClientGUIManagement.CreateManagementControllerImportSimpleDownloader() return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook ) diff --git a/include/ClientGUIParsing.py b/include/ClientGUIParsing.py index 7f436a5e..d1a9c6be 100644 --- a/include/ClientGUIParsing.py +++ b/include/ClientGUIParsing.py @@ -2203,18 +2203,12 @@ The formula should attempt to parse full or relative urls. If the url is relativ def EventTestParse( self, event ): - node = self.GetValue() - - try: + def wx_code( parsed_urls ): - stop_time = HydrusData.GetNow() + 30 - - job_key = ClientThreading.JobKey( cancellable = True, stop_time = stop_time ) - - data = self._example_data.GetValue() - referral_url = self._referral_url - - parsed_urls = node.ParseURLs( job_key, data, referral_url ) + if not self: + + return + if len( parsed_urls ) > 0: @@ -2232,15 +2226,40 @@ The formula should attempt to parse full or relative urls. If the url is relativ self._results.SetValue( results_text ) - except Exception as e: + + def do_it( node, data, referral_url ): - HydrusData.ShowException( e ) - - message = 'Could not parse!' - - wx.MessageBox( message ) + try: + + stop_time = HydrusData.GetNow() + 30 + + job_key = ClientThreading.JobKey( cancellable = True, stop_time = stop_time ) + + parsed_urls = node.ParseURLs( job_key, data, referral_url ) + + wx.CallAfter( wx_code, parsed_urls ) + + except Exception as e: + + HydrusData.ShowException( e ) + + message = 'Could not parse!' + + wx.CallAfter( wx.MessageBox, message ) + + node = self.GetValue() + data = self._example_data.GetValue() + referral_url = self._referral_url + + HG.client_controller.CallToThread( do_it, node, data, referral_url ) + + + def GetExampleData( self ): + + return self._example_data.GetValue() + def GetExampleURL( self ): @@ -3100,19 +3119,12 @@ And pass that html to a number of 'parsing children' that will each look through def EventTestParse( self, event ): - script = self.GetValue() - - try: + def wx_code( results ): - stop_time = HydrusData.GetNow() + 30 - - job_key = ClientThreading.JobKey( cancellable = True, stop_time = stop_time ) - - self._test_script_management.SetJobKey( job_key ) - - data = self._example_data.GetValue() - - results = script.Parse( job_key, data ) + if not self: + + return + result_lines = [ '*** ' + HydrusData.ConvertIntToPrettyString( len( results ) ) + ' RESULTS BEGIN ***' ] @@ -3124,19 +3136,41 @@ And pass that html to a number of 'parsing children' that will each look through self._results.SetValue( results_text ) - except Exception as e: + + def do_it( script, job_key, data ): - HydrusData.ShowException( e ) - - message = 'Could not parse!' - - wx.MessageBox( message ) - - finally: - - job_key.Finish() + try: + + results = script.Parse( job_key, data ) + + wx.CallAfter( wx_code, results ) + + except Exception as e: + + HydrusData.ShowException( e ) + + message = 'Could not parse!' + + wx.CallAfter( wx.MessageBox, message ) + + finally: + + job_key.Finish() + + script = self.GetValue() + + stop_time = HydrusData.GetNow() + 30 + + job_key = ClientThreading.JobKey( cancellable = True, stop_time = stop_time ) + + self._test_script_management.SetJobKey( job_key ) + + data = self._example_data.GetValue() + + HG.client_controller.CallToThread( do_it, script, job_key, data ) + def GetExampleData( self ): @@ -4414,6 +4448,9 @@ class TestPanel( wx.Panel ): self._copy_button = ClientGUICommon.BetterBitmapButton( self, CC.GlobalBMPs.copy, self._Copy ) self._copy_button.SetToolTip( 'Copy the current example data to the clipboard.' ) + self._fetch_button = ClientGUICommon.BetterBitmapButton( self, CC.GlobalBMPs.link, self._FetchFromURL ) + self._fetch_button.SetToolTip( 'Fetch data from a URL.' ) + self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.GlobalBMPs.paste, self._Paste ) self._paste_button.SetToolTip( 'Paste the current clipboard data into here.' ) @@ -4446,6 +4483,7 @@ class TestPanel( wx.Panel ): buttons_hbox = wx.BoxSizer( wx.HORIZONTAL ) buttons_hbox.Add( self._copy_button, CC.FLAGS_VCENTER ) + buttons_hbox.Add( self._fetch_button, CC.FLAGS_VCENTER ) buttons_hbox.Add( self._paste_button, CC.FLAGS_VCENTER ) desc_hbox = wx.BoxSizer( wx.HORIZONTAL ) @@ -4469,6 +4507,54 @@ class TestPanel( wx.Panel ): HG.client_controller.pub( 'clipboard', 'text', self._example_data ) + def _FetchFromURL( self ): + + def wx_code( example_data ): + + self._SetExampleData( example_data ) + + + def do_it( url ): + + network_job = ClientNetworking.NetworkJob( 'GET', url ) + + network_job.OverrideBandwidth() + + HG.client_controller.network_engine.AddJob( network_job ) + + try: + + network_job.WaitUntilDone() + + example_data = network_job.GetContent() + + except HydrusExceptions.CancelledException: + + example_data = 'fetch cancelled' + + except Exception as e: + + example_data = 'fetch failed:' + os.linesep * 2 + HydrusData.ToUnicode( e ) + + HydrusData.ShowException( e ) + + + wx.CallAfter( wx_code, example_data ) + + + message = 'Enter URL to fetch data for.' + + with ClientGUIDialogs.DialogTextEntry( self, message, default = 'enter url', allow_blank = False) as dlg: + + if dlg.ShowModal() == wx.ID_OK: + + url = dlg.GetValue() + + HG.client_controller.CallToThread( do_it, url ) + + + + def _Paste( self ): raw_text = HG.client_controller.GetClipboardText() diff --git a/include/ClientGUIScrolledPanelsReview.py b/include/ClientGUIScrolledPanelsReview.py index 311f0b41..1136b6ce 100644 --- a/include/ClientGUIScrolledPanelsReview.py +++ b/include/ClientGUIScrolledPanelsReview.py @@ -1908,8 +1908,23 @@ class MigrateDatabasePanel( ClientGUIScrolledPanels.ReviewPanel ): pretty_portable = 'no' - free_space = HydrusPaths.GetFreeSpace( location ) - pretty_free_space = HydrusData.ConvertIntToBytes( free_space ) + try: + + free_space = HydrusPaths.GetFreeSpace( location ) + pretty_free_space = HydrusData.ConvertIntToBytes( free_space ) + + except Exception as e: + + HydrusData.ShowException( e ) + + message = 'There was a problem finding the free space for "' + location + '"! Perhaps this location does not exist?' + + wx.MessageBox( message ) + HydrusData.ShowText( message ) + + free_space = 0 + pretty_free_space = 'problem finding free space' + fp = locations_to_file_weights[ location ] / 256.0 tp = locations_to_fs_thumb_weights[ location ] / 256.0 diff --git a/include/ClientGUISeedCache.py b/include/ClientGUISeedCache.py index 52ef0330..58a4d1ae 100644 --- a/include/ClientGUISeedCache.py +++ b/include/ClientGUISeedCache.py @@ -33,7 +33,7 @@ class EditSeedCachePanel( ClientGUIScrolledPanels.EditPanel ): columns = [ ( '#', 3 ), ( 'source', -1 ), ( 'status', 12 ), ( 'added', 23 ), ( 'last modified', 23 ), ( 'source time', 23 ), ( 'note', 20 ) ] - self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self, 'seed_cache', 30, 30, columns, self._ConvertSeedToListCtrlTuples ) + self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self, 'seed_cache', 30, 30, columns, self._ConvertSeedToListCtrlTuples, delete_key_callback = self._DeleteSelected ) # diff --git a/include/ClientImporting.py b/include/ClientImporting.py index 677e54e3..88bae8e3 100644 --- a/include/ClientImporting.py +++ b/include/ClientImporting.py @@ -2713,618 +2713,6 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ): HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_IMPORT_FOLDER ] = ImportFolder -class PageOfImagesImport( HydrusSerialisable.SerialisableBase ): - - SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PAGE_OF_IMAGES_IMPORT - SERIALISABLE_NAME = 'Page Of Images Import' - SERIALISABLE_VERSION = 2 - - def __init__( self ): - - HydrusSerialisable.SerialisableBase.__init__( self ) - - file_import_options = HG.client_controller.new_options.GetDefaultFileImportOptions( 'loud' ) - - self._pending_page_urls = [] - self._seed_cache = SeedCache() - self._file_import_options = file_import_options - self._download_image_links = True - self._download_unlinked_images = False - self._queue_paused = False - self._files_paused = False - - self._parser_status = '' - self._current_action = '' - - self._download_control_file_set = None - self._download_control_file_clear = None - self._download_control_page_set = None - self._download_control_page_clear = None - - self._lock = threading.Lock() - - self._new_files_event = threading.Event() - self._new_page_event = threading.Event() - - - def _GetSerialisableInfo( self ): - - serialisable_seed_cache = self._seed_cache.GetSerialisableTuple() - serialisable_file_options = self._file_import_options.GetSerialisableTuple() - - return ( self._pending_page_urls, serialisable_seed_cache, serialisable_file_options, self._download_image_links, self._download_unlinked_images, self._queue_paused, self._files_paused ) - - - def _InitialiseFromSerialisableInfo( self, serialisable_info ): - - ( self._pending_page_urls, serialisable_seed_cache, serialisable_file_options, self._download_image_links, self._download_unlinked_images, self._queue_paused, self._files_paused ) = serialisable_info - - self._seed_cache = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_seed_cache ) - self._file_import_options = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_options ) - - - def _UpdateSerialisableInfo( self, version, old_serialisable_info ): - - if version == 1: - - ( pending_page_urls, serialisable_seed_cache, serialisable_file_options, download_image_links, download_unlinked_images, paused ) = old_serialisable_info - - queue_paused = paused - files_paused = paused - - new_serialisable_info = ( pending_page_urls, serialisable_seed_cache, serialisable_file_options, download_image_links, download_unlinked_images, queue_paused, files_paused ) - - return ( 2, new_serialisable_info ) - - - - def _WorkOnFiles( self, page_key ): - - seed = self._seed_cache.GetNextSeed( CC.STATUS_UNKNOWN ) - - if seed is None: - - return - - - did_substantial_work = False - - file_url = seed.seed_data - - try: - - with self._lock: - - self._current_action = 'reviewing file' - - - ( status, hash, note ) = HG.client_controller.Read( 'url_status', file_url ) - - url_not_known_beforehand = status == CC.STATUS_NEW - - if status == CC.STATUS_DELETED: - - if not self._file_import_options.ExcludesDeleted(): - - status = CC.STATUS_NEW - note = '' - - - - if status == CC.STATUS_NEW: - - ( os_file_handle, temp_path ) = HydrusPaths.GetTempPath() - - try: - - with self._lock: - - self._current_action = 'downloading file' - - - network_job = ClientNetworking.NetworkJob( 'GET', file_url, temp_path = temp_path ) - - HG.client_controller.network_engine.AddJob( network_job ) - - with self._lock: - - if self._download_control_file_set is not None: - - wx.CallAfter( self._download_control_file_set, network_job ) - - - - try: - - network_job.WaitUntilDone() - - except HydrusExceptions.ShutdownException: - - return - - except HydrusExceptions.CancelledException: - - status = CC.STATUS_SKIPPED - - seed.SetStatus( status, note = 'cancelled during download!' ) - - return - - except HydrusExceptions.NotFoundException: - - status = CC.STATUS_FAILED - note = '404' - - seed.SetStatus( status, note = note ) - - time.sleep( 2 ) - - return - - except HydrusExceptions.NetworkException: - - status = CC.STATUS_FAILED - - seed.SetStatus( status, note = network_job.GetErrorText() ) - - time.sleep( 2 ) - - return - - finally: - - if self._download_control_file_clear is not None: - - wx.CallAfter( self._download_control_file_clear ) - - - - with self._lock: - - self._current_action = 'importing file' - - - file_import_job = FileImportJob( temp_path, self._file_import_options ) - - ( status, hash ) = HG.client_controller.client_files_manager.ImportFile( file_import_job ) - - did_substantial_work = True - - seed.SetStatus( status ) - - if url_not_known_beforehand and hash is not None: - - service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( hash, ( file_url, ) ) ) ] } - - HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates ) - - did_substantial_work = True - - - finally: - - HydrusPaths.CleanUpTempPath( os_file_handle, temp_path ) - - - else: - - seed.SetStatus( status, note = note ) - - - in_inbox = HG.client_controller.Read( 'in_inbox', hash ) - - if self._file_import_options.ShouldPresent( status, in_inbox ): - - ( media_result, ) = HG.client_controller.Read( 'media_results', ( hash, ) ) - - HG.client_controller.pub( 'add_media_results', page_key, ( media_result, ) ) - - did_substantial_work = True - - - except HydrusExceptions.MimeException as e: - - status = CC.STATUS_UNINTERESTING_MIME - - seed.SetStatus( status ) - - except HydrusExceptions.NotFoundException: - - status = CC.STATUS_FAILED - note = '404' - - seed.SetStatus( status, note = note ) - - time.sleep( 2 ) - - except Exception as e: - - status = CC.STATUS_FAILED - - seed.SetStatus( status, exception = e ) - - time.sleep( 3 ) - - finally: - - self._seed_cache.NotifySeedsUpdated( ( seed, ) ) - - with self._lock: - - self._current_action = '' - - - - if did_substantial_work: - - time.sleep( DID_SUBSTANTIAL_FILE_WORK_MINIMUM_SLEEP_TIME ) - - - - def _WorkOnQueue( self, page_key ): - - if len( self._pending_page_urls ) > 0: - - with self._lock: - - page_url = self._pending_page_urls.pop( 0 ) - - self._parser_status = 'checking ' + page_url - - - error_occurred = False - - try: - - network_job = ClientNetworking.NetworkJob( 'GET', page_url ) - - network_job.OverrideBandwidth() - - HG.client_controller.network_engine.AddJob( network_job ) - - with self._lock: - - if self._download_control_page_set is not None: - - wx.CallAfter( self._download_control_page_set, network_job ) - - - - try: - - network_job.WaitUntilDone() - - finally: - - if self._download_control_page_clear is not None: - - wx.CallAfter( self._download_control_page_clear ) - - - - html = network_job.GetContent() - - soup = ClientDownloading.GetSoup( html ) - - # - - all_links = soup.find_all( 'a' ) - - links_with_images = [ link for link in all_links if len( link.find_all( 'img' ) ) > 0 ] - - all_linked_images = [] - - for link in all_links: - - images = link.find_all( 'img' ) - - all_linked_images.extend( images ) - - - all_images = soup.find_all( 'img' ) - - unlinked_images = [ image for image in all_images if image not in all_linked_images ] - - # - - file_urls = [] - - if self._download_image_links: - - file_urls.extend( [ urlparse.urljoin( page_url, link[ 'href' ] ) for link in links_with_images if link.has_attr( 'href' ) ] ) - - - if self._download_unlinked_images: - - file_urls.extend( [ urlparse.urljoin( page_url, image[ 'src' ] ) for image in unlinked_images if image.has_attr( 'src' ) ] ) - - - seeds = [ Seed( SEED_TYPE_URL, url ) for url in file_urls ] - - num_new = self._seed_cache.AddSeeds( seeds ) - - if num_new > 0: - - self._new_files_event.set() - - - parser_status = 'page checked OK - ' + HydrusData.ConvertIntToPrettyString( num_new ) + ' new urls' - - num_already_in_seed_cache = len( file_urls ) - num_new - - if num_already_in_seed_cache > 0: - - parser_status += ' (' + HydrusData.ConvertIntToPrettyString( num_already_in_seed_cache ) + ' already in queue)' - - - except HydrusExceptions.ShutdownException: - - return - - except HydrusExceptions.NotFoundException: - - error_occurred = True - - parser_status = 'page 404' - - except Exception as e: - - error_occurred = True - - parser_status = HydrusData.ToUnicode( e ) - - - with self._lock: - - self._parser_status = parser_status - - - if error_occurred: - - time.sleep( 5 ) - - - return True - - else: - - with self._lock: - - self._parser_status = '' - - - return False - - - - def _THREADWorkOnFiles( self, page_key ): - - while not ( HG.view_shutdown or HG.client_controller.PageCompletelyDestroyed( page_key ) ): - - no_work_to_do = self._files_paused or not self._seed_cache.WorkToDo() - - if no_work_to_do or HG.client_controller.PageClosedButNotDestroyed( page_key ): - - self._new_files_event.wait( 5 ) - - else: - - try: - - self._WorkOnFiles( page_key ) - - HG.client_controller.WaitUntilViewFree() - - except Exception as e: - - HydrusData.ShowException( e ) - - return - - - - self._new_files_event.clear() - - - - def _THREADWorkOnQueue( self, page_key ): - - while not ( HG.view_shutdown or HG.client_controller.PageCompletelyDestroyed( page_key ) ): - - if self._queue_paused or HG.client_controller.PageClosedButNotDestroyed( page_key ): - - self._new_page_event.wait( 5 ) - - else: - - try: - - did_work = self._WorkOnQueue( page_key ) - - if did_work: - - time.sleep( 5 ) - - else: - - self._new_page_event.wait( 5 ) - - - HG.client_controller.WaitUntilViewFree() - - except Exception as e: - - HydrusData.ShowException( e ) - - return - - - - self._new_page_event.clear() - - - - def AdvancePageURL( self, page_url ): - - with self._lock: - - if page_url in self._pending_page_urls: - - index = self._pending_page_urls.index( page_url ) - - if index - 1 >= 0: - - self._pending_page_urls.remove( page_url ) - - self._pending_page_urls.insert( index - 1, page_url ) - - - - - - def CurrentlyWorking( self ): - - with self._lock: - - finished = not self._seed_cache.WorkToDo() or len( self._pending_page_urls ) > 0 - - return not finished and not self._files_paused - - - - def DelayPageURL( self, page_url ): - - with self._lock: - - if page_url in self._pending_page_urls: - - index = self._pending_page_urls.index( page_url ) - - if index + 1 < len( self._pending_page_urls ): - - self._pending_page_urls.remove( page_url ) - - self._pending_page_urls.insert( index + 1, page_url ) - - - - - - def DeletePageURL( self, page_url ): - - with self._lock: - - if page_url in self._pending_page_urls: - - self._pending_page_urls.remove( page_url ) - - - - - def GetSeedCache( self ): - - return self._seed_cache - - - def GetOptions( self ): - - with self._lock: - - return ( self._file_import_options, self._download_image_links, self._download_unlinked_images ) - - - - def GetStatus( self ): - - with self._lock: - - return ( list( self._pending_page_urls ), self._parser_status, self._current_action, self._queue_paused, self._files_paused ) - - - - def PausePlayFiles( self ): - - with self._lock: - - self._files_paused = not self._files_paused - - self._new_files_event.set() - - - - def PausePlayQueue( self ): - - with self._lock: - - self._queue_paused = not self._queue_paused - - self._new_page_event.set() - - - - def PendPageURL( self, page_url ): - - with self._lock: - - if page_url not in self._pending_page_urls: - - self._pending_page_urls.append( page_url ) - - self._new_page_event.set() - - - - - def SetDownloadControlFile( self, download_control ): - - with self._lock: - - self._download_control_file_set = download_control.SetNetworkJob - self._download_control_file_clear = download_control.ClearNetworkJob - - - - def SetDownloadControlPage( self, download_control ): - - with self._lock: - - self._download_control_page_set = download_control.SetNetworkJob - self._download_control_page_clear = download_control.ClearNetworkJob - - - - def SetDownloadImageLinks( self, value ): - - with self._lock: - - self._download_image_links = value - - - - def SetDownloadUnlinkedImages( self, value ): - - with self._lock: - - self._download_unlinked_images = value - - - - def SetFileImportOptions( self, file_import_options ): - - with self._lock: - - self._file_import_options = file_import_options - - - - def Start( self, page_key ): - - HG.client_controller.CallToThreadLongRunning( self._THREADWorkOnQueue, page_key ) - HG.client_controller.CallToThreadLongRunning( self._THREADWorkOnFiles, page_key ) - - -HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_PAGE_OF_IMAGES_IMPORT ] = PageOfImagesImport - SEED_TYPE_HDD = 0 SEED_TYPE_URL = 1 @@ -4135,6 +3523,588 @@ class SeedCache( HydrusSerialisable.SerialisableBase ): HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_SEED_CACHE ] = SeedCache +class SimpleDownloaderImport( HydrusSerialisable.SerialisableBase ): + + SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SIMPLE_DOWNLOADER_IMPORT + SERIALISABLE_NAME = 'Simple Downloader Import' + SERIALISABLE_VERSION = 3 + + def __init__( self ): + + HydrusSerialisable.SerialisableBase.__init__( self ) + + file_import_options = HG.client_controller.new_options.GetDefaultFileImportOptions( 'loud' ) + + self._pending_jobs = [] + self._seed_cache = SeedCache() + self._file_import_options = file_import_options + self._queue_paused = False + self._files_paused = False + + self._parser_status = '' + self._current_action = '' + + self._download_control_file_set = None + self._download_control_file_clear = None + self._download_control_page_set = None + self._download_control_page_clear = None + + self._lock = threading.Lock() + + self._new_files_event = threading.Event() + self._new_page_event = threading.Event() + + + def _GetSerialisableInfo( self ): + + serialisable_pending_jobs = [ ( url, formula_name, formula.GetSerialisableTuple() ) for ( url, formula_name, formula ) in self._pending_jobs ] + + serialisable_seed_cache = self._seed_cache.GetSerialisableTuple() + serialisable_file_options = self._file_import_options.GetSerialisableTuple() + + return ( serialisable_pending_jobs, serialisable_seed_cache, serialisable_file_options, self._queue_paused, self._files_paused ) + + + def _InitialiseFromSerialisableInfo( self, serialisable_info ): + + ( serialisable_pending_jobs, serialisable_seed_cache, serialisable_file_options, self._queue_paused, self._files_paused ) = serialisable_info + + self._pending_jobs = [ ( url, formula_name, HydrusSerialisable.CreateFromSerialisableTuple( serialisable_formula ) ) for ( url, formula_name, serialisable_formula ) in serialisable_pending_jobs ] + + self._seed_cache = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_seed_cache ) + self._file_import_options = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_options ) + + + def _UpdateSerialisableInfo( self, version, old_serialisable_info ): + + if version == 1: + + ( pending_page_urls, serialisable_seed_cache, serialisable_file_options, download_image_links, download_unlinked_images, paused ) = old_serialisable_info + + queue_paused = paused + files_paused = paused + + new_serialisable_info = ( pending_page_urls, serialisable_seed_cache, serialisable_file_options, download_image_links, download_unlinked_images, queue_paused, files_paused ) + + return ( 2, new_serialisable_info ) + + + if version == 2: + + ( pending_page_urls, serialisable_seed_cache, serialisable_file_options, download_image_links, download_unlinked_images, queue_paused, files_paused ) = old_serialisable_info + + pending_jobs = [] + + new_serialisable_info = ( pending_jobs, serialisable_seed_cache, serialisable_file_options, queue_paused, files_paused ) + + return ( 3, new_serialisable_info ) + + + + def _WorkOnFiles( self, page_key ): + + seed = self._seed_cache.GetNextSeed( CC.STATUS_UNKNOWN ) + + if seed is None: + + return + + + did_substantial_work = False + + file_url = seed.seed_data + + try: + + with self._lock: + + self._current_action = 'reviewing file' + + + ( status, hash, note ) = HG.client_controller.Read( 'url_status', file_url ) + + url_not_known_beforehand = status == CC.STATUS_NEW + + if status == CC.STATUS_DELETED: + + if not self._file_import_options.ExcludesDeleted(): + + status = CC.STATUS_NEW + note = '' + + + + if status == CC.STATUS_NEW: + + ( os_file_handle, temp_path ) = HydrusPaths.GetTempPath() + + try: + + with self._lock: + + self._current_action = 'downloading file' + + + network_job = ClientNetworking.NetworkJob( 'GET', file_url, temp_path = temp_path ) + + HG.client_controller.network_engine.AddJob( network_job ) + + with self._lock: + + if self._download_control_file_set is not None: + + wx.CallAfter( self._download_control_file_set, network_job ) + + + + try: + + network_job.WaitUntilDone() + + except HydrusExceptions.ShutdownException: + + return + + except HydrusExceptions.CancelledException: + + status = CC.STATUS_SKIPPED + + seed.SetStatus( status, note = 'cancelled during download!' ) + + return + + except HydrusExceptions.NotFoundException: + + status = CC.STATUS_FAILED + note = '404' + + seed.SetStatus( status, note = note ) + + time.sleep( 2 ) + + return + + except HydrusExceptions.NetworkException: + + status = CC.STATUS_FAILED + + seed.SetStatus( status, note = network_job.GetErrorText() ) + + time.sleep( 2 ) + + return + + finally: + + if self._download_control_file_clear is not None: + + wx.CallAfter( self._download_control_file_clear ) + + + + with self._lock: + + self._current_action = 'importing file' + + + file_import_job = FileImportJob( temp_path, self._file_import_options ) + + ( status, hash ) = HG.client_controller.client_files_manager.ImportFile( file_import_job ) + + did_substantial_work = True + + seed.SetStatus( status ) + + if url_not_known_beforehand and hash is not None: + + service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( hash, ( file_url, ) ) ) ] } + + HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates ) + + did_substantial_work = True + + + finally: + + HydrusPaths.CleanUpTempPath( os_file_handle, temp_path ) + + + else: + + seed.SetStatus( status, note = note ) + + + in_inbox = HG.client_controller.Read( 'in_inbox', hash ) + + if self._file_import_options.ShouldPresent( status, in_inbox ): + + ( media_result, ) = HG.client_controller.Read( 'media_results', ( hash, ) ) + + HG.client_controller.pub( 'add_media_results', page_key, ( media_result, ) ) + + did_substantial_work = True + + + except HydrusExceptions.MimeException as e: + + status = CC.STATUS_UNINTERESTING_MIME + + seed.SetStatus( status ) + + except HydrusExceptions.NotFoundException: + + status = CC.STATUS_FAILED + note = '404' + + seed.SetStatus( status, note = note ) + + time.sleep( 2 ) + + except Exception as e: + + status = CC.STATUS_FAILED + + seed.SetStatus( status, exception = e ) + + time.sleep( 3 ) + + finally: + + self._seed_cache.NotifySeedsUpdated( ( seed, ) ) + + with self._lock: + + self._current_action = '' + + + + if did_substantial_work: + + time.sleep( DID_SUBSTANTIAL_FILE_WORK_MINIMUM_SLEEP_TIME ) + + + + def _WorkOnQueue( self, page_key ): + + if len( self._pending_jobs ) > 0: + + with self._lock: + + ( url, formula_name, formula ) = self._pending_jobs.pop( 0 ) + + self._parser_status = 'checking ' + url + + + error_occurred = False + + try: + + network_job = ClientNetworking.NetworkJob( 'GET', url ) + + network_job.OverrideBandwidth() + + HG.client_controller.network_engine.AddJob( network_job ) + + with self._lock: + + if self._download_control_page_set is not None: + + wx.CallAfter( self._download_control_page_set, network_job ) + + + + try: + + network_job.WaitUntilDone() + + finally: + + if self._download_control_page_clear is not None: + + wx.CallAfter( self._download_control_page_clear ) + + + + data = network_job.GetContent() + + # + + parsing_context = {} + + parsing_context[ 'url' ] = url + + file_urls = [ urlparse.urljoin( url, parsed_text ) for parsed_text in formula.Parse( parsing_context, data ) ] + + seeds = [ Seed( SEED_TYPE_URL, file_url ) for file_url in file_urls ] + + num_new = self._seed_cache.AddSeeds( seeds ) + + if num_new > 0: + + self._new_files_event.set() + + + parser_status = 'page checked OK - ' + HydrusData.ConvertIntToPrettyString( num_new ) + ' new urls' + + num_already_in_seed_cache = len( file_urls ) - num_new + + if num_already_in_seed_cache > 0: + + parser_status += ' (' + HydrusData.ConvertIntToPrettyString( num_already_in_seed_cache ) + ' already in queue)' + + + except HydrusExceptions.ShutdownException: + + return + + except HydrusExceptions.NotFoundException: + + error_occurred = True + + parser_status = 'page 404' + + except Exception as e: + + error_occurred = True + + parser_status = HydrusData.ToUnicode( e ) + + + with self._lock: + + self._parser_status = parser_status + + + if error_occurred: + + time.sleep( 5 ) + + + return True + + else: + + with self._lock: + + self._parser_status = '' + + + return False + + + + def _THREADWorkOnFiles( self, page_key ): + + while not ( HG.view_shutdown or HG.client_controller.PageCompletelyDestroyed( page_key ) ): + + no_work_to_do = self._files_paused or not self._seed_cache.WorkToDo() + + if no_work_to_do or HG.client_controller.PageClosedButNotDestroyed( page_key ): + + self._new_files_event.wait( 5 ) + + else: + + try: + + self._WorkOnFiles( page_key ) + + HG.client_controller.WaitUntilViewFree() + + except Exception as e: + + HydrusData.ShowException( e ) + + return + + + + self._new_files_event.clear() + + + + def _THREADWorkOnQueue( self, page_key ): + + while not ( HG.view_shutdown or HG.client_controller.PageCompletelyDestroyed( page_key ) ): + + if self._queue_paused or HG.client_controller.PageClosedButNotDestroyed( page_key ): + + self._new_page_event.wait( 5 ) + + else: + + try: + + did_work = self._WorkOnQueue( page_key ) + + if did_work: + + time.sleep( 5 ) + + else: + + self._new_page_event.wait( 5 ) + + + HG.client_controller.WaitUntilViewFree() + + except Exception as e: + + HydrusData.ShowException( e ) + + return + + + + self._new_page_event.clear() + + + + def AdvanceJob( self, job ): + + with self._lock: + + if job in self._pending_jobs: + + index = self._pending_jobs.index( job ) + + if index - 1 >= 0: + + self._pending_jobs.remove( job ) + + self._pending_jobs.insert( index - 1, job ) + + + + + + def CurrentlyWorking( self ): + + with self._lock: + + finished = not self._seed_cache.WorkToDo() or len( self._pending_jobs ) > 0 + + return not finished and not self._files_paused + + + + def DelayJob( self, job ): + + with self._lock: + + if job in self._pending_jobs: + + index = self._pending_jobs.index( job ) + + if index + 1 < len( self._pending_jobs ): + + self._pending_jobs.remove( job ) + + self._pending_jobs.insert( index + 1, job ) + + + + + + def DeleteJob( self, job ): + + with self._lock: + + if job in self._pending_jobs: + + self._pending_jobs.remove( job ) + + + + + def GetSeedCache( self ): + + return self._seed_cache + + + def GetFileImportOptions( self ): + + with self._lock: + + return self._file_import_options + + + + def GetStatus( self ): + + with self._lock: + + return ( list( self._pending_jobs ), self._parser_status, self._current_action, self._queue_paused, self._files_paused ) + + + + def PausePlayFiles( self ): + + with self._lock: + + self._files_paused = not self._files_paused + + self._new_files_event.set() + + + + def PausePlayQueue( self ): + + with self._lock: + + self._queue_paused = not self._queue_paused + + self._new_page_event.set() + + + + def PendJob( self, job ): + + with self._lock: + + if job not in self._pending_jobs: + + self._pending_jobs.append( job ) + + self._new_page_event.set() + + + + + def SetDownloadControlFile( self, download_control ): + + with self._lock: + + self._download_control_file_set = download_control.SetNetworkJob + self._download_control_file_clear = download_control.ClearNetworkJob + + + + def SetDownloadControlPage( self, download_control ): + + with self._lock: + + self._download_control_page_set = download_control.SetNetworkJob + self._download_control_page_clear = download_control.ClearNetworkJob + + + + def SetFileImportOptions( self, file_import_options ): + + with self._lock: + + self._file_import_options = file_import_options + + + + def Start( self, page_key ): + + HG.client_controller.CallToThreadLongRunning( self._THREADWorkOnQueue, page_key ) + HG.client_controller.CallToThreadLongRunning( self._THREADWorkOnFiles, page_key ) + + +HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_SIMPLE_DOWNLOADER_IMPORT ] = SimpleDownloaderImport + class Subscription( HydrusSerialisable.SerialisableBaseNamed ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION @@ -4259,6 +4229,25 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ): return HydrusData.TimeHasPassed( self._no_work_until ) + def _QueryBandwidthIsOK( self, query ): + + example_network_contexts = self._GetExampleNetworkContexts( query ) + + # just a little padding here + expected_requests = 3 + expected_bytes = 1048576 + threshold = 30 + + result = HG.client_controller.network_engine.bandwidth_manager.CanDoWork( example_network_contexts, expected_requests = expected_requests, expected_bytes = expected_bytes, threshold = threshold ) + + if HG.subscription_report_mode: + + HydrusData.ShowText( 'Query "' + query.GetQueryText() + '" pre-work Bandwidth test. Bandwidth ok: ' + str( result ) + '.' ) + + + return result + + def _ShowHitPeriodicFileLimitMessage( self, query_text ): message = 'When syncing, the query "' + query_text + '" for subscription "' + self._name + '" hit its periodic file limit!' @@ -4398,6 +4387,11 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ): if seed is None: + if HG.subscription_report_mode: + + HydrusData.ShowText( 'Query "' + query_text + '" can do no more file work due to running out of unknown urls.' ) + + break @@ -4412,15 +4406,7 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ): p1 = HC.options[ 'pause_subs_sync' ] p3 = HG.view_shutdown - - example_nj = network_job_factory( 'GET', url ) - - # just a little padding, to make sure we don't accidentally get into a long wait because we need to fetch file and tags independantly etc... - expected_requests = 3 - expected_bytes = 1048576 - threshold = 600 - - p4 = not HG.client_controller.network_engine.bandwidth_manager.CanDoWork( example_nj.GetNetworkContexts(), expected_requests = expected_requests, expected_bytes = expected_bytes, threshold = threshold ) + p4 = not self._QueryBandwidthIsOK( query ) if p1 or p3 or p4: @@ -4630,14 +4616,7 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ): if query.CanWorkOnFiles(): - example_network_contexts = self._GetExampleNetworkContexts( query ) - - # just a little padding here - expected_requests = 3 - expected_bytes = 1048576 - threshold = 30 - - if HG.client_controller.network_engine.bandwidth_manager.CanDoWork( example_network_contexts, expected_requests = expected_requests, expected_bytes = expected_bytes, threshold = threshold ): + if self._QueryBandwidthIsOK( query ): return True @@ -4649,11 +4628,20 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ): def _SyncQuery( self, job_key ): + have_made_an_initial_sync_bandwidth_notification = False + queries = self._GetQueriesForProcessing() for query in queries: - if not query.CanSync(): + can_sync = query.CanSync() + + if HG.subscription_report_mode: + + HydrusData.ShowText( 'Query "' + query.GetQueryText() + '" started. Current can_sync is ' + str( can_sync ) + '.' ) + + + if not can_sync: continue @@ -4852,7 +4840,26 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ): if query.IsDead(): - HydrusData.ShowText( 'The query "' + query_text + '" for subscription ' + self._name + ' appears to be dead!' ) + if this_is_initial_sync: + + HydrusData.ShowText( 'The query "' + query_text + '" for subscription "' + self._name + '" did not find any files on its first sync! Could the query text have a typo, like a missing underscore?' ) + + else: + + HydrusData.ShowText( 'The query "' + query_text + '" for subscription "' + self._name + '" appears to be dead!' ) + + + else: + + if this_is_initial_sync: + + if not self._QueryBandwidthIsOK( query ) and not have_made_an_initial_sync_bandwidth_notification: + + HydrusData.ShowText( 'FYI: The query "' + query_text + '" for subscription "' + self._name + '" performed its initial sync ok, but that domain is short on bandwidth right now, so no files will be downloaded yet. The subscription will catch up in future as bandwidth becomes available. You can review the estimated time until bandwidth is available under the manage subscriptions dialog. If more queries are performing initial syncs in this run, they be the same.' ) + + have_made_an_initial_sync_bandwidth_notification = True + + @@ -5068,6 +5075,21 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ): p4 = self._SyncQueryCanDoWork() p5 = self._WorkOnFilesCanDoWork() + if HG.subscription_report_mode: + + message = 'Subscription "' + self._name + '" entered sync.' + message += os.linesep + message += 'Unpaused: ' + str( p1 ) + message += os.linesep + message += 'No delays: ' + str( p3 ) + message += os.linesep + message += 'Sync can do work: ' + str( p4 ) + message += os.linesep + message += 'Files can do work: ' + str( p5 ) + + HydrusData.ShowText( message ) + + if p1 and p2 and p3 and ( p4 or p5 ): job_key = ClientThreading.JobKey( pausable = False, cancellable = True ) @@ -5171,6 +5193,11 @@ class SubscriptionQuery( HydrusSerialisable.SerialisableBase ): seed = self._seed_cache.GetNextSeed( CC.STATUS_UNKNOWN ) + if HG.subscription_report_mode: + + HydrusData.ShowText( 'Query "' + self._query + '" CanWorkOnFiles test. Next import is ' + repr( seed ) + '.' ) + + return seed is not None @@ -5186,6 +5213,11 @@ class SubscriptionQuery( HydrusSerialisable.SerialisableBase ): def CanSync( self ): + if HG.subscription_report_mode: + + HydrusData.ShowText( 'Query "' + self._query + '" CanSync test. Paused status is ' + str( self._paused ) + ' and check time due is ' + str( HydrusData.TimeHasPassed( self._next_check_time ) ) + ' and check_now is ' + str( self._check_now ) + '.' ) + + if self._paused: return False @@ -6334,6 +6366,14 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ): + def IsDead( self ): + + with self._lock: + + return self._thread_status in ( CHECKER_STATUS_404, CHECKER_STATUS_DEAD ) + + + def PausePlayFiles( self ): with self._lock: diff --git a/include/ClientMedia.py b/include/ClientMedia.py index 30a8f153..7366d699 100644 --- a/include/ClientMedia.py +++ b/include/ClientMedia.py @@ -958,7 +958,9 @@ class MediaList( object ): if media.IsCollection(): - media_results.extend( media.GenerateMediaResults( has_location = has_location, discriminant = discriminant, selected_media = selected_media, unrated = unrated, for_media_viewer = True ) ) + # don't include selected_media here as it is not valid at the deeper collection level + + media_results.extend( media.GenerateMediaResults( has_location = has_location, discriminant = discriminant, unrated = unrated, for_media_viewer = True ) ) else: @@ -1489,68 +1491,6 @@ class MediaSingleton( Media ): return self._media_result.GetHash() - def MatchesDiscriminant( self, has_location = None, discriminant = None, not_uploaded_to = None ): - - if discriminant is not None: - - inbox = self._media_result.GetInbox() - - locations_manager = self._media_result.GetLocationsManager() - - if discriminant == CC.DISCRIMINANT_INBOX: - - p = inbox - - elif discriminant == CC.DISCRIMINANT_ARCHIVE: - - p = not inbox - - elif discriminant == CC.DISCRIMINANT_LOCAL: - - p = locations_manager.IsLocal() - - elif discriminant == CC.DISCRIMINANT_LOCAL_BUT_NOT_IN_TRASH: - - p = locations_manager.IsLocal() and not locations_manager.IsTrashed() - - elif discriminant == CC.DISCRIMINANT_NOT_LOCAL: - - p = not locations_manager.IsLocal() - - elif discriminant == CC.DISCRIMINANT_DOWNLOADING: - - p = locations_manager.IsDownloading() - - - if not p: - - return False - - - - if has_location is not None: - - locations_manager = self._media_result.GetLocationsManager() - - if has_location not in locations_manager.GetCurrent(): - - return False - - - - if not_uploaded_to is not None: - - locations_manager = self._media_result.GetLocationsManager() - - if not_uploaded_to in locations_manager.GetCurrentRemote(): - - return False - - - - return True - - def GetHashes( self, has_location = None, discriminant = None, not_uploaded_to = None, ordered = False ): if self.MatchesDiscriminant( has_location = has_location, discriminant = discriminant, not_uploaded_to = not_uploaded_to ): @@ -1730,6 +1670,68 @@ class MediaSingleton( Media ): def IsSizeDefinite( self ): return self._media_result.GetSize() is not None + def MatchesDiscriminant( self, has_location = None, discriminant = None, not_uploaded_to = None ): + + if discriminant is not None: + + inbox = self._media_result.GetInbox() + + locations_manager = self._media_result.GetLocationsManager() + + if discriminant == CC.DISCRIMINANT_INBOX: + + p = inbox + + elif discriminant == CC.DISCRIMINANT_ARCHIVE: + + p = not inbox + + elif discriminant == CC.DISCRIMINANT_LOCAL: + + p = locations_manager.IsLocal() + + elif discriminant == CC.DISCRIMINANT_LOCAL_BUT_NOT_IN_TRASH: + + p = locations_manager.IsLocal() and not locations_manager.IsTrashed() + + elif discriminant == CC.DISCRIMINANT_NOT_LOCAL: + + p = not locations_manager.IsLocal() + + elif discriminant == CC.DISCRIMINANT_DOWNLOADING: + + p = locations_manager.IsDownloading() + + + if not p: + + return False + + + + if has_location is not None: + + locations_manager = self._media_result.GetLocationsManager() + + if has_location not in locations_manager.GetCurrent(): + + return False + + + + if not_uploaded_to is not None: + + locations_manager = self._media_result.GetLocationsManager() + + if not_uploaded_to in locations_manager.GetCurrentRemote(): + + return False + + + + return True + + def RefreshFileInfo( self ): self._media_result.RefreshFileInfo() diff --git a/include/HydrusConstants.py b/include/HydrusConstants.py index b22a32fa..e6755c76 100755 --- a/include/HydrusConstants.py +++ b/include/HydrusConstants.py @@ -49,7 +49,7 @@ options = {} # Misc NETWORK_VERSION = 18 -SOFTWARE_VERSION = 300 +SOFTWARE_VERSION = 301 UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 ) diff --git a/include/HydrusGlobals.py b/include/HydrusGlobals.py index 554e35a1..b1d49e96 100644 --- a/include/HydrusGlobals.py +++ b/include/HydrusGlobals.py @@ -16,6 +16,7 @@ db_report_mode = False db_profile_mode = False gui_report_mode = False shortcut_report_mode = False +subscription_report_mode = False hover_window_report_mode = False menu_profile_mode = False network_report_mode = False diff --git a/include/HydrusSerialisable.py b/include/HydrusSerialisable.py index 3a8b5db0..171687ab 100644 --- a/include/HydrusSerialisable.py +++ b/include/HydrusSerialisable.py @@ -32,7 +32,7 @@ SERIALISABLE_TYPE_PREDICATE = 14 SERIALISABLE_TYPE_FILE_SEARCH_CONTEXT = 15 SERIALISABLE_TYPE_EXPORT_FOLDER = 16 SERIALISABLE_TYPE_THREAD_WATCHER_IMPORT = 17 -SERIALISABLE_TYPE_PAGE_OF_IMAGES_IMPORT = 18 +SERIALISABLE_TYPE_SIMPLE_DOWNLOADER_IMPORT = 18 SERIALISABLE_TYPE_IMPORT_FOLDER = 19 SERIALISABLE_TYPE_GALLERY_IMPORT = 20 SERIALISABLE_TYPE_DICTIONARY = 21 diff --git a/include/TestDB.py b/include/TestDB.py index c0e3ff5a..03448755 100644 --- a/include/TestDB.py +++ b/include/TestDB.py @@ -662,7 +662,7 @@ class TestClientDB( unittest.TestCase ): # - management_controller = ClientGUIManagement.CreateManagementControllerImportPageOfImages() + management_controller = ClientGUIManagement.CreateManagementControllerImportSimpleDownloader() page = ClientGUIPages.Page( test_frame, HG.test_controller, management_controller, [] ) @@ -744,7 +744,7 @@ class TestClientDB( unittest.TestCase ): - self.assertEqual( page_names, [ u'hentai foundry artist', u'import', u'thread watcher', u'page download', u'example tag repo petitions', u'search', u'search', u'files', u'wew lad', u'files' ] ) + self.assertEqual( page_names, [ u'hentai foundry artist', u'import', u'thread watcher', u'simple downloader', u'example tag repo petitions', u'search', u'search', u'files', u'wew lad', u'files' ] ) finally: