From bac0421831011afe4c552a0e8931fd4691068452 Mon Sep 17 00:00:00 2001 From: Hydrus Network Developer Date: Wed, 25 Jan 2023 16:59:39 -0600 Subject: [PATCH] Version 514 --- docs/advanced_sidecars.md | 134 + docs/changelog.md | 138 +- docs/developer_api.md | 434 +-- docs/developer_api_future.md | 2318 ----------------- docs/getting_started_importing.md | 8 +- docs/images/sidecars_example_json_export.png | Bin 0 -> 17481 bytes docs/images/sidecars_example_json_import.png | Bin 0 -> 68820 bytes .../images/sidecars_example_manual_export.png | Bin 0 -> 94272 bytes .../images/sidecars_example_manual_import.png | Bin 0 -> 125535 bytes docs/old_changelog.html | 75 + docs/running_from_source.md | 86 +- hydrus/client/ClientCaches.py | 58 +- hydrus/client/ClientConstants.py | 5 + hydrus/client/ClientFiles.py | 75 +- hydrus/client/ClientOptions.py | 2 + hydrus/client/ClientSearch.py | 41 +- hydrus/client/db/ClientDB.py | 351 +++ hydrus/client/db/ClientDBFilesStorage.py | 2 +- hydrus/client/gui/ClientGUIGallerySeedLog.py | 9 +- .../gui/ClientGUIScrolledPanelsManagement.py | 15 +- .../gui/ClientGUIScrolledPanelsReview.py | 28 +- hydrus/client/gui/ClientGUISubscriptions.py | 2 +- hydrus/client/gui/ClientGUITagSuggestions.py | 136 +- hydrus/client/gui/ClientGUITags.py | 21 +- hydrus/client/gui/canvas/ClientGUICanvas.py | 2 +- .../client/gui/exporting/ClientGUIExport.py | 21 + hydrus/client/gui/lists/ClientGUIListBoxes.py | 15 + .../client/gui/pages/ClientGUIManagement.py | 19 +- hydrus/client/gui/pages/ClientGUIPages.py | 27 +- .../client/gui/search/ClientGUIACDropdown.py | 47 +- .../services/ClientGUIClientsideServices.py | 1 - .../client/importing/ClientImportFileSeeds.py | 467 +++- .../importing/ClientImportGallerySeeds.py | 90 +- hydrus/client/importing/ClientImportLocal.py | 4 +- .../ClientMetadataMigrationExporters.py | 2 +- .../ClientMetadataMigrationImporters.py | 3 + hydrus/client/networking/ClientLocalServer.py | 2 +- .../networking/ClientLocalServerResources.py | 781 +++--- hydrus/core/HydrusConstants.py | 4 +- hydrus/core/HydrusData.py | 8 +- hydrus/core/HydrusText.py | 1 + .../HydrusNetworkVariableHandling.py | 156 +- hydrus/test/TestClientAPI.py | 267 +- hydrus/test/TestClientDBDuplicates.py | 28 +- hydrus/test/TestClientTags.py | 8 +- mkdocs.yml | 1 + .../gugs/twitter collection lookup.png | Bin 2396 -> 0 bytes static/default/gugs/twitter likes lookup.png | Bin 2263 -> 0 bytes static/default/gugs/twitter list lookup.png | Bin 2305 -> 0 bytes .../twitter profile lookup (with replies).png | Bin 2773 -> 2800 bytes .../default/gugs/twitter profile lookup.png | Bin 2270 -> 2287 bytes ...oru file page parser - get webm ugoira.png | Bin 2730 -> 3320 bytes .../parsers/deviant art file page parser.png | Bin 3775 -> 4206 bytes .../parsers/pixiv file page api parser.png | Bin 2991 -> 3082 bytes ...yndication api timeline-profile parser.png | Bin 0 -> 3421 bytes .../twitter syndication api tweet parser.png | Bin 3446 -> 3480 bytes static/default/url_classes/twitter list.png | Bin 1906 -> 0 bytes .../twitter syndication api collection.png | Bin 2592 -> 0 bytes ...witter syndication api likes (user_id).png | Bin 2880 -> 0 bytes .../twitter syndication api likes.png | Bin 2610 -> 0 bytes ...twitter syndication api list (list_id).png | Bin 2763 -> 0 bytes ...cation api list (screen_name and slug).png | Bin 3394 -> 0 bytes ...yndication api list (user_id and slug).png | Bin 3009 -> 0 bytes ...tter syndication api profile (user_id).png | Bin 2921 -> 0 bytes .../twitter syndication api profile.png | Bin 2612 -> 0 bytes ...itter syndication api timeline-profile.png | Bin 0 -> 2807 bytes .../twitter syndication api tweet-result.png | Bin 2648 -> 2642 bytes .../twitter syndication api tweet.png | Bin 2500 -> 0 bytes .../twitter tweet (i_web_status).png | Bin 0 -> 2471 bytes static/default/url_classes/twitter tweet.png | Bin 1879 -> 1901 bytes 70 files changed, 2431 insertions(+), 3461 deletions(-) create mode 100644 docs/advanced_sidecars.md delete mode 100644 docs/developer_api_future.md create mode 100644 docs/images/sidecars_example_json_export.png create mode 100644 docs/images/sidecars_example_json_import.png create mode 100644 docs/images/sidecars_example_manual_export.png create mode 100644 docs/images/sidecars_example_manual_import.png delete mode 100644 static/default/gugs/twitter collection lookup.png delete mode 100644 static/default/gugs/twitter likes lookup.png delete mode 100644 static/default/gugs/twitter list lookup.png create mode 100644 static/default/parsers/twitter syndication api timeline-profile parser.png delete mode 100644 static/default/url_classes/twitter list.png delete mode 100644 static/default/url_classes/twitter syndication api collection.png delete mode 100644 static/default/url_classes/twitter syndication api likes (user_id).png delete mode 100644 static/default/url_classes/twitter syndication api likes.png delete mode 100644 static/default/url_classes/twitter syndication api list (list_id).png delete mode 100644 static/default/url_classes/twitter syndication api list (screen_name and slug).png delete mode 100644 static/default/url_classes/twitter syndication api list (user_id and slug).png delete mode 100644 static/default/url_classes/twitter syndication api profile (user_id).png delete mode 100644 static/default/url_classes/twitter syndication api profile.png create mode 100644 static/default/url_classes/twitter syndication api timeline-profile.png delete mode 100644 static/default/url_classes/twitter syndication api tweet.png create mode 100644 static/default/url_classes/twitter tweet (i_web_status).png diff --git a/docs/advanced_sidecars.md b/docs/advanced_sidecars.md new file mode 100644 index 00000000..89218f19 --- /dev/null +++ b/docs/advanced_sidecars.md @@ -0,0 +1,134 @@ +--- +title: Sidecars +--- + +# sidecars + +Sidecars are files that provide additional metadata about a master file. They typically share the same basic filename--if the master is 'Image_123456.jpg', the sidecar will be something like 'Image_123456.txt' or 'Image_123456.jpg.json'. This obviously makes it easy to figure out which sidecar goes with which file. + +Hydrus does not use sidecars in its own storage, but it can import data from them and export data to them. It currently supports raw data in .txt files and encoded data in .json files, and that data can be either tags or URLs. I expect to extend this system in future to support XML and other metadata types such as ratings, timestamps, and inbox/archive status. + +We'll start with .txt, since they are simpler. + +## Importing Sidecars + +Imagine you have some jpegs you downloaded with another program. That program grabbed the files' tags somehow, and you want to import the files with their tags without messing around with the Client API. + +If your extra program can export the tags to a simple format--let's say newline-separated .txt files with the same basic filename as the jpegs, or you can, with some very simple scripting, convert to that format--then importing them to hydrus is easy! + +Put the jpegs and the .txt files in the same directory and then drag and drop the directory onto the client, as you would for a normal import. The .txt files should not be added to the list. Then click 'add tags/urls with the import'. The sidecars are managed on one of the tabs: + +[![](images/sidecars_example_manual_import.png)](images/sidecars_example_manual_import.png) + +This system can get quite complicated, but the essential idea is that you are selecting one or more sidecar `sources`, parsing their text, and sending that list of data to one hydrus service `destination`. Most of the time you will be pulling from just one sidecar at a time. + +### The Source Dialog + +The `source` is a description of a sidecar to load and how to read what it contains. + +In this example, the texts are like so: + +``` title="4e01850417d1978e6328d4f40c3b550ef582f8558539b4ad46a1cb7650a2e10b.jpg.txt" +flowers +landscape +blue sky +``` + +``` title="5e390f043321de57cb40fd7ca7cf0cfca29831670bd4ad71622226bc0a057876.jpg.txt" +fast car +anime girl +night sky +``` + +Since our sidecars in this example are named (filename.ext).txt, and use newlines as the separator character, we can leave things mostly as default. + +If you do not have newline-separated tags, for instance comma-separated tags (`flowers, landscape, blue sky`), then you can set that here. Be careful if you are making your own sidecars, since any separator character obviously cannot be used in tag text! + +If your sidecars are named (filename).txt instead of (filename.ext).txt, then just hit the checkbox, but if the conversion is more complicated, then play around with the filename string converter and the test boxes. + +If you need to, you can further process the texts that are loaded. They'll be trimmed of extra whitespace and so on automatically, so no need to worry about that, but if you need to, let's say, add the `creator:` prefix to everything, or filter out some mis-parsed garbage, this is the place. + +### The Router Dialog + +A 'Router' is a single set of orders to grab from one or more sidecars and send to a destination. You can have several routers in a single import or export context. + +You can do more string processing here, and it will apply to everything loaded from every sidecar. + +The destination is either a tag service (adding the loaded strings as tags), or your known URLs store. + +### Previewing + +Once you have something set up, you can see the results are live-loaded in the dialog. Make sure everything looks all correct, and then start the import as normal and you should see the tags or URLs being added as the import works. + +It is good to try out some simple situations with one or two files just to get a feel for the system. + +### Import Folders + +If you have a constant flow of sidecar-attached media, then you can add sidecars to Import Folders too. Do a trial-run of anything you want to parse with a manual import before setting up the automatic system. + +## Exporting Sidecars + +The rules for exporting are similar, but now you are pulling from one or more hydrus service `sources` and sending to a single `destination` sidecar every time. Let's look at the UI: + +[![](images/sidecars_example_manual_export.png)](images/sidecars_example_manual_export.png) + +I have chosen to select these files' URLs and send them to newline-separated .urls.txt files. If I wanted to get the tags too, I could pull from one or more tag services, filter and convert the tags as needed, and then output to a .tags.txt file. + +The best way to learn with this is just to experiment. The UI may seem intimidating, but most jobs don't need you to work with multiple sidecars or string processing or clever filenames. + +## JSON Files + +JSON is more complicated than .txt. You might have multiple metadata types all together in one file, so you may end up setting up multiple routers that parse the same file for different content, or for an export you might want to populate the same export file with multiple kinds of content. Hydrus can do it! + +### Importing + +Since JSON files are richly structured, we will have to dip into the Hydrus parsing system: + +[![](images/sidecars_example_json_import.png)](images/sidecars_example_json_import.png) + +If you have made a downloader before, you will be familiar with this. If not, then you can brave [the help](downloader_parsers_formulae.md#json_formula) or just have a play around with the UI. In this example, I am getting the URL(s) of each JSON file, which are stored in a list under the `file_info_urls` key. + +It is important to paste an example JSON file that you want to parse into the parsing testing area (click the paste button) so you can test on read data live. + +Once you have the parsing set up, the rest of the sidecar UI is the same as for .txt. The JSON Parsing formula is just the replacement/equivalent for the .txt 'separator' setting. + +_Note that you could set up a second Router to import the tags from this file!_ + +### Exporting + +In Hydrus, the exported JSON is typically a nested Object with a similar format as in the Import example. You set the names of the Object keys. + +[![](images/sidecars_example_json_export.png)](images/sidecars_example_json_export.png) + +Here I have set the URLs of each file to be stored under `metadata->urls`, which will make this sort of structure: + +``` json +{ + "metadata" : { + "urls" : [ + "http://example.com/123456", + "https://site.org/post/45678" + ] + } +} +``` + +The cool thing about JSON files is I can export multiple times to the same file and it will update it! Lets say I made a second Router that grabbed the tags, and it was set to export to the same filename but under `metadata->tags`. The final sidecar would look like this: + +``` json +{ + "metadata" : { + "tags" : [ + "blonde hair", + "blue eyes", + "skirt" + ], + "urls" : [ + "http://example.com/123456", + "https://site.org/post/45678" + ] + } +} +``` + +You should be careful that the location you are exporting to does not have any old JSON files with conflicting filenames in it--hydrus will update them, not overwrite them! This may be an issue if you have an synchronising Export Folder that exports random files with the same filenames. diff --git a/docs/changelog.md b/docs/changelog.md index fa310d3b..65ac0820 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,95 @@ title: Changelog !!! note This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html). +## [Version 514](https://github.com/hydrusnetwork/hydrus/releases/tag/v514) + +### downloaders + +* twitter took down the API we were using, breaking all our nice twitter downloaders! argh! +* a user has figured out a basic new downloader that grabs the tweets amongst the first twenty tweets-and-retweets of an account. yes, only the first twenty max, and usually fewer. because this is a big change, the client will ask about it when you update. if you have some complicated situation where you are working on the old default twitter downloaders and don't want them deleted, you can select 'no' on the dialog it throws up, but everyone else wants to say 'yes'. then check your twitter subs: make sure they moved to the new downloader, and you probably want to make them check more frequently too. +* given the rate of changes at twitter, I think we can expect more changes and blocks in future. I don't know whether nitter will be viable alternative, so if the artists you like end up on a nice simple booru _anywhere_, I strongly recommend just moving there. twitter appears to be explicitly moving to non-third-party-friendly +* thanks to a user's work, the 'danbooru - get webm ugoira' parser is fixed! +* thanks to a user's work, the deviant art parser is updated to get the highest res image in more situations! +* thanks to a user's work, the pixiv downloader now gets the artist note, in japanese (and translated, if there is one), and a 'medium:ai generated' tag! + +### sidecars + +* I wrote some sidecar help here! https://hydrusnetwork.github.io/hydrus/advanced_sidecars.html +* when the client parses files for import, the 'does this look like a sidecar?' test now also checks that the base component of the base filename (e.g. 'Image123' from 'Image123.jpg.txt') actually appears in the list of non-txt/json/xml ext files. a random yo.txt file out of nowhere will now be inspected in case it is secretly a jpeg again, for good or ill +* when you drop some files on the client, the number of files skipped because they looked like sidecars is now stated in the status label +* fixed a typo bug that meant tags imported from sidecars were not being properly cleaned, despite preview appearance otherwise, for instance ':)', which in hydrus needs to be secretly stored as '::)' was being imported as ')' +* as a special case, tags that in hydrus are secretly '::)' will be converted to ':)' on export to sidecar too, the inverse of the above problem. there may be some other tag cleaning quirks to undo here, so let me know what you run into + +### related tags overhaul + +* the 'related tags' suggestion system, turned on under _options->tag suggestions_, has several changes, including some prototype tech I'd love feedback on +* first off, there are two new search buttons, 'new 1' and 'new 2' ('2' is available on repositories only).. these use an upgraded statistical search and scoring system that a user worked on and sent in. I have butchered his specific namespace searching system to something more general/flexible and easy for me to maintain, but it works better and more comprehensibly than my old method! give it a go and let me know how each button does--the first one will be fast but less useful on the PTR, the second will be slower but generally give richer results (although it cannot do tags with too-high count) +* the new search routine works on multiple files, so 'related tags' now shows on tag dialogs launched from a selection of thumbnails! +* also, all the related search buttons now search any selection of tags you make!!! so if you can't remember that character's name, just click on the series or another character they are often with and hit the search, and you should get a whole bunch appear +* I am going to keep working on this in the future. the new buttons will become the only buttons, I'll try and mitigate the prototype search limitations, add some cancel tech, move to a time-based search length like the current buttons, and I'll add more settings, including for filtering so we aren't looking up related tags for 'page:x' and so on. I'm interested in knowing how you get on with IRL data. are there too many recommendations (is the tolerance too high?)? is the sorting good (is the stuff at the top relevant or often just noise?)? + +### misc + +* all users can now copy their service keys (which are a technical non-changing hex identifier for your client's services) from the review services window--advanced mode is no longer needed. this may be useful as the client api transitions to service keys +* when a job in the downloader search log generates new jobs (e.g. fetches the next page), the new job(s) are now inserted after the parent. previously, they were appended to the end of the list. this changes how ngugs operate, converting their searches from interleaved to sequential! +* restarting search log jobs now also places the new job after the restarted job +* when you create a new export folder, if you have default metadata export sidecar settings from a previous manual file export, the program now asks if you want those for the new export folder or an empty list. previously, it just assigned the saved default, which could be jarring if it was saved from ages ago +* added a migration guide to the running from source help. also brushed up some language and fixed a bunch of borked title weights in that document +* the max initial and periodic file limits in subscriptions is now 50k when in advanced mode. I can't promise that would be nice though! +* the file history chart no longer says that inbox and delete time tracking are new + +### misc fixes + +* fixed a cursor type detection test that was stopping the cursor from hiding immediately when you do a media viewer drag in Qt6 +* fixed an issue where 'clear deletion record' calls were not deleting from the newer 'all my files' domain. the erroneous extra records will be searched for and scrubbed on update +* fixed the issue where if you had the new 'unnamespaced input gives (any namespace) wildcard results' search option on, you couldn't add any novel tags in WRITE autocomplete contexts like 'manage tags'!!! it could only offer the automatically converted wildcard tags as suggested input, which of course aren't appropriate for a WRITE context. the way I ultimately fixed this was horrible; the whole thing needs more work to deal with clever logic like this better, so let me know if you get any more trouble here +* I think I fixed an infinite hang when trying to add certain siblings in manage tag siblings. I believe this was occuring when the dialog was testing if the new pair would create a loop when the sibling structure already contains a loop. now it throws up a message and breaks the test +* fixed an issue where certain system:filetype predicates would spawn apparent duplicates of themselves instead of removing on double-click. images+audio+video+swf+pdf was one example. it was a 'all the image types' vs 'list of (all the) image types' conversion/comparison/sorting issue + +### client api + +* **this is later than I expected, but as was planned last year, I am clearing up several obsolete parameters and data structures this week. mostly it is bad service name-identification that seemed simple or flexible to support but just added maintenance debt, induced bad implementation practises, and hindered future expansions. if you have a custom api script, please read on--and if you have not yet moved to the alternatives, do so before updating!** +* **all `...service_name...` parameters are officially obsolete! they will still work via some legacy hacks, so old scripts shouldn't break, but they are no longer documented. please move to the `...service_key...` alternates as soon as reasonably possible (check out `/get_services` if you need to learn about service keys)** +* **`/add_tags/get_tag_services` is removed! use `/get_services` instead!** +* **`hide_service_names_tags`, previously made default true, is removed and its data structures `service_names_to_statuses_to_...` are also gone! move to the new `tags` structure.** +* **`hide_service_keys_tags` is now default true. it will be removed in 4 weeks or so. same deal as with `service_names_to_statuses_to_...`--move to `tags`** +* **`system_inbox` and `system_archive` are removed from `/get_files/search_files`! just use 'system:inbox/archive' in the tags list** +* **the 'set_file_relationships' command from last week has been reworked to have a nicer Object parameter with a new name. please check the updated help!** normally I wouldn't change something so quick, but we are still in early prototype, so I'm ok shifting it (and the old method still works lmao, but I'll clear that code out in a few weeks, so please move over--the Object will be much nicer to expand in future, which I forgot about in v513) +### many Client API commands now support modern file domain objects, meaning you can search a UNION of file services and 'deleted-from' file services. affected commands are + +* * /add_files/delete_files +* * /add_files/undelete_files +* * /add_tags/search_tags +* * /get_files/search_files +* * /manage_file_relationships/get_everything +* a new `/get_service` call now lets you ask about an individual service by service name or service key, basically a parameterised /get_services +* the `/manage_pages/get_pages` and `/manage_pages/get_page_info` calls now give the `page_state`, a new enum that says if the page is ready, initialised, searching, or search-cancelled +* to reduce duplicate argument spam, the client api help now specifies the complicated 'these files' and now 'this file domain' arguments into sub-sections, and the commands that use them just point to the subsections. check it out--it makes sense when you look at it. +* `/add_tags/add_tags` now raises 400 if you give an invalid content action (e.g. pending to a local tag service). previously it skipped these rows silently +* added and updated unit tests and help for the above changes +* client api version is now 41 + +### boring optimisation + +* when you are looking at a search log or file log, if entries are added, removed, or moved around, all the log entries that have changed row # now update (previously it just sent a redraw signal for the new rows, not the second-order affected rows that were shuffled up/down. many access routines for these logs are sped up +* file log status checking is completely rewritten. the ways it searches, caches and optimises the 'which is the next item with x status' queues is faster and requires far less maintenance. large import queues have less overhead, so the in and outs of general download work should scale up much better now +* the main data cache that stores rendered images, image tiles, and thumbnails now maintains itself far more efficiently. there was a hellish O(n) overhead when adding or removing an item which has been reduced to constant time. this gonk was being spammed every few minutes during normal memory maintenance, when hundreds of thumbs can be purged at once. clients with tens of thousands of thumbnails in memory will maintain that list far more smoothly +* physical file delete is now more efficient, requiring far fewer hard drive hits to delete a media file. it is also far less aggressive, with a new setting in _options->files and trash_ that sets how long to wait between individual file deletes, default 250ms. before, it was full LFG mode with minor delays every hundred/thousand jobs, and since it takes a write lock, it was lagging out thumbnail load when hitting a lot of work. the daemon here also shuts down faster if caught working during program shut down + +### boring code cleanup + +* refactored some parsing routines to be more flexible +* added some more dictionary and enum type testing to the client api parameter parsing routines. error messages should be better! +* improved how `/add_tags/add_tags` parsing works. ensuring both access methods check all types and report nicer errors +* cleaned up the `/search_files/file_metadata` call's parsing, moving to the new generalised method and smoothing out some old code flow. it now checks hashes against the last search, too +* cleaned up `/manage_pages/add_files` similarly +* cleaned up how tag services are parsed and their errors reported in the client api +* the client api is better about processing the file identifiers you give it in the same order you gave +* fixed bad 'potentials_search_type'/'search_type' inconsistency in the client api help examples +* obviously a bunch of client api unit test and help cleanup to account for the obsolete stuff and various other changes here +* updated a bunch of the client api unit tests to handle some of the new parsing +* fixed the remaining 'randomly fail due to complex counting logic' potential count unit tests. turns out there were like seven more of them + ## [Version 513](https://github.com/hydrusnetwork/hydrus/releases/tag/v513) ### client api @@ -430,52 +519,3 @@ title: Changelog * cleaned up some edge cases in the 'which account added this file/mapping to the server?' tech, where it might have been possible, when looking up deleted content, to get another janitor account (i.e. who deleted the content), although I am pretty sure this situation was never possible to actually start in UI. if I add 'who deleted this?' tech in future, it'll be a separate specific call * cleaned up some specifically 'Qt6' references in the build script. the build requirements.txts and spec files are also collapsed down, with old Qt5 versions removed * filled out some incomplete abstract class definitions - -## [Version 504](https://github.com/hydrusnetwork/hydrus/releases/tag/v504) - -### Qt5 -* as a reminder, I am no longer supporting Qt5 with the official builds. if you are on Windows 7 (and I have heard at least one version of Win 8.1), or a similarly old OS, you likely cannot run the official builds now. if this is you, please check the 'running from source' guide in the help, which will allow you to keep updating the program. this process is now easy in Windows and should be similarly easy on other platforms soon - -### misc -* if you run from source in windows, the program _should_ now have its own taskbar group and use the correct hydrus icon. if you try and pin it to taskbar, it will revert to the 'python' icon, but you can give a shortcut to a batch file an icon and pin that to start -* unfortunately, I have to remove the 'deviant art tag search' downloader this week. they killed the old API we were using, and what remaining open date-paginated search results the site offers is obfuscated and tokenised (no permanent links), more than I could quickly unravel. other downloader creators are welcome to give it a go. if you have a subscription for a da tag search, it will likely complain on its next run. please pause it and try to capture the best artists from that search (until DA kill their free artist api, then who knows what will happen). the oauth/phone app menace marches on -* focus on the thumbnail panel is now preserved whenever it swaps out for another (like when you refresh the search) -* fixed an issue where cancelling service selection on database->c&r->repopulate truncated would create an empty modal message -* fixed a stupid typo in the recently changed server petition counting auto-fixing code - -### importer/exporter sidecar expansion -* when you import or export files from/to disk, either manually or automatically, the option to pull or send tags to .txt files is now expanded: -* - you can now import or export URLs -* - you can now read or write .json files -* - you can now import from or export to multiple sidecars, and have multiple separate pipelines -* - you can now give sidecar files suffixes, for ".tags.txt" and similar -* - you can now filter and transform all the strings in this pipeline using the powerful String Processor just like in the parsing system -* this affects manual imports, manual exports, import folders, and export folders. instead of smart .txt checkboxes, there's now a button leading to some nested dialogs to customise your 'routers' and, in manual imports, a new page tab in the 'add tags before import' window -* this bones of this system was already working in the background when I introduced it earlier this year, but now all components are exposed -* new export folders now start with the same default metadata migration as set in the last manual file export dialog -* this system will expand in future. most important is to add a 'favourites' system so you can easily save/load your different setups. then adding more content types (e.g. ratings) and .xml. I'd also like to add purely internal file-to-itself datatype transformation (e.g. pulling url:(url) tags and converting them to actual known urls, and vice versa) - -### importer/exporter sidecar expansion (boring stuff) -* split the importer/exporter objects into separate importers and exporters. existing router objects will update and split their internal objects safely -* all objects in this system can now describe themselves -* all import/export nodes now produce appropriate example texts for string processing and parsing UI test panels -* Filename Tagging Options objects no longer track neighbouring .txt file importing, and their UI removes it too. Import Folders will suck their old data on update and convert to metadata routers -* wrote a json sidecar importer that takes a parsing formula -* wrote a json sidecar exporter that takes a list of dictionary names to export to. it will edit an existing file -* wrote some ui panels to edit single file metadata migration routers -* wrote some ui panels to edit single file metadata migration importers -* wrote some ui panels to edit single file metadata migration exporters -* updated edit export folder panel to use the new UI. it was already using a full static version of the system behind the scenes; now this is exposed and editable -* updated the manual file export panel to use the new UI. it was using a half version of the system before--now the default options are updated to the new router object and you can create multiple exports -* updated import folders to use the new UI. the filename tagging options no longer handles .txt, it is now on a separate button on the import folder -* updated manual file imports to use the new UI. the 'add tags before import' window now has a 'sidecars' page tab, which lets you edit metadata routers. it updates a path preview list live with what it expects to parse -* a full suite of new unit tests now checks the router, the four import nodes, and the four export nodes thoroughly -* renamed ClientExportingMetadata to ClientMetadataMigration and moved to the metadata module. refactored the importers, exporters, and shared methods to their own files in the same module -* created a gui.metadata module for the new router and metadata import/export widgets and panels -* created a gui.exporting module for the existing export folder and manual export gui code -* reworked some of the core importer/exporter objects and inheritance in clientmetadatamigration -* updated the HDDImport object and creation pipeline to handle metadata routers (as piped from the new sidecars tab) -* when the hdd import or import folder is set to delete original files, now all defined sidecars are deleted along with the media file -* cleaned up a bunch of related metadata importer/exporter code -* cleaned import folder code -* cleaned hdd importer code diff --git a/docs/developer_api.md b/docs/developer_api.md index c7568003..c4df886e 100644 --- a/docs/developer_api.md +++ b/docs/developer_api.md @@ -106,6 +106,68 @@ Session keys will expire if they are not used within 24 hours, or if the client Bear in mind the Client API is still under construction. Setting up the Client API to be accessible across the internet requires technical experience to be convenient. HTTPS is available for encrypted comms, but the default certificate is self-signed (which basically means an eavesdropper can't see through it, but your ISP/government could if they decided to target you). If you have your own domain and SSL cert, you can replace them though (check the db directory for client.crt and client.key). Otherwise, be careful about transmitting sensitive content outside of your localhost/network. +## Common Complex Parameters + +### **files** { id="parameters_files" } + +If you need to refer to some files, you can use any of the following: + +Arguments: +: + * `file_id`: (selective, a numerical file id) + * `file_ids`: (selective, a list of numerical file ids) + * `hash`: (selective, a hexadecimal SHA256 hash) + * `hashes`: (selective, a list of hexadecimal SHA256 hashes) + +In GET requests, make sure any list is percent-encoded. + +### **file domain** { id="parameters_file_domain" } + +When you are searching, you may want to specify a particular file domain. Most of the time, you'll want to just set `file_service_key`, but this can get complex: + +Arguments: +: + * `file_service_key`: (optional, selective A, hexadecimal, the file domain on which to search) + * `file_service_keys`: (optional, selective A, list of hexadecimals, the union of file domains on which to search) + * `deleted_file_service_key`: (optional, selective B, hexadecimal, the 'deleted from this file domain' on which to search) + * `deleted_file_service_keys`: (optional, selective B, list of hexadecimals, the union of 'deleted from this file domain' on which to search) + +The service keys are as in [/get\_services](#get_services). + +Hydrus supports two concepts here: + +* Searching over a UNION of subdomains. If the user has several local file domains, e.g. 'favourites', 'personal', 'sfw', and 'nsfw', they might like to search two of them at once. +* Searching deleted files of subdomains. You can specifically, and quickly, search the files that have been deleted from somewhere. + +You can play around with this yourself by clicking 'multiple locations' in the client with _help->advanced mode_ on. + +In extreme edge cases, these two can be mixed by populating both A and B selective, making a larger union of both current and deleted file records. + +Please note that unions can be very very computationally expensive. If you can achieve what you want with a single file_service_key, two queries in a row with different service keys, or an umbrella like `all my files` or `all local files`, please do. Otherwise, let me know what is running slow and I'll have a look at it. + +'deleted from all local files' includes all files that have been physically deleted (i.e. deleted from the trash) and not available any more for fetch file/thumbnail requests. 'deleted from all my files' includes all of those physically deleted files _and_ the trash. If a file is deleted with the special 'do not leave a deletion record' command, then it won't show up in a 'deleted from file domain' search! + +'all known files' is a tricky domain. It converts much of the search tech to ignore where files actually are and look at the accompanying tag domain (e.g. all the files that have been tagged), and can sometimes be very expensive. + +Also, if you have the option to set both file and tag domains, you cannot enter 'all known files'/'all known tags'. It is too complicated to support, sorry! + +### **legacy service_name parameters** { id="legacy_service_name_parameters" } + +The Client API used to respond to name-based service identifiers, for instance using 'my tags' instead of something like '6c6f63616c2074616773'. Service names can change, and they aren't _strictly_ unique either, so I have moved away from them, but there is some soft legacy support. + +The client will attempt to convert any of these to their 'service_key(s)' equivalents: + +* file_service_name +* tag_service_name +* service_names_to_tags +* service_names_to_actions_to_tags +* service_names_to_additional_tags + +But I strongly encourage you to move away from them as soon as reasonably possible. Look up the service keys you need with [/get\_service](#get_service) or [/get\_services](#get_services). + +If you have a clever script/program that does many things, then hit up [/get\_services](#get_services) on session initialisation and cache an internal map of key_to_name for the labels to use when you present services to the user. + +Also, note that all users can now copy their service keys from _review services_. ## Access Management @@ -209,6 +271,44 @@ Response: ``` +### **GET `/get_service`** { id="get_service" } + +_Ask the client about a specific service._ + +Restricted access: +: YES. At least one of Add Files, Add Tags, Manage Pages, or Search Files permission needed. + +Required Headers: n/a + +Arguments: +: + * `service_name`: (selective, string, the name of the service) + * `service_key`: (selective, hex string, the service key of the service) + +Example requests: +: + ```title="Example requests" + /get_service?service_name=my%20tags + /get_service?service_key=6c6f63616c2074616773 + ``` + +Response: +: Some JSON about the service. The same basic format as [/get\_services](#get_services) +```json title="Example response" +{ + "service" : { + "name" : "my tags", + "service_key" : "6c6f63616c2074616773", + "type" : 5, + "type_pretty" : "local tag service" + } +} +``` + +If the service does not exist, this gives 404. It is very unlikely but edge-case possible that two services will have the same name, in this case you'll get the pseudorandom first. + +It will only respond to services in the /get_services list. I will expand the available types in future as we add ratings etc... to the Client API. + ### **GET `/get_services`** { id="get_services" } _Ask the client about its file and tag services._ @@ -312,11 +412,14 @@ Response: ] } ``` - These services may be referred to in various metadata responses or required in request parameters (e.g. where to add tag mappings). Note that a user can rename their services. The older parts of the Client API use the renameable 'service name' as service identifier, but wish to move away from this. Please use the hex 'service_key', which is a non-mutable ID specific to each client. The hardcoded services have shorter service key strings (it is usually just 'all known files' etc.. ASCII-converted to hex), but user-made stuff will have 64-character hex. + + Note that a user can rename their services, so while they will recognise `name`, it is not an excellent identifier, and definitely not something to save to any permanent config file. - Now that I state `type` and `type_pretty` here, I may rearrange this call, probably to make the `service_key` the Object key, rather than the arbitrary 'all_known_tags' strings. + `service_key` is non-mutable and is the main service identifier. The hardcoded/initial services have shorter fixed service key strings (it is usually just 'all known files' etc.. ASCII-converted to hex), but user-created services will have random 64-character hex. - You won't see all these, and you'll only ever need some, but `type` is: + Now that I state `type` and `type_pretty` here, I may rearrange this call, probably into a flat list. The `all_known_files` Object keys here are arbitrary. + + For service `type`, you won't see all these, and you'll only ever need some, but the enum is: * 0 - tag repository * 1 - file repository @@ -337,6 +440,7 @@ Response: * 21 - all my files -- union of all local file domains * 99 - server administration + `type_pretty` is something you can show users if you like. Hydrus uses the same labels in _manage services_ and so on. ## Importing and Deleting Files @@ -396,12 +500,8 @@ Required Headers: Arguments (in JSON): : -* `hash`: (an SHA256 hash for a file in 64 characters of hexadecimal) -* `hashes`: (a list of SHA256 hashes) -* `file_id`: (a numerical file id) -* `file_ids`: (a list of numerical file ids) -* `file_service_name`: (optional, selective, string, the local file domain from which to delete, or all local files) -* `file_service_key`: (optional, selective, hexadecimal, the local file domain from which to delete, or all local files) +* [files](#parameters_files) +* [file domain](#parameters_file_domain) (optional, defaults to 'all my files') * `reason`: (optional, string, the reason attached to the delete action) ```json title="Example request body" @@ -411,9 +511,7 @@ Arguments (in JSON): Response: : 200 and no content. -You can use hash or hashes, whichever is more convenient. - -If you specify a file service, the file will only be deleted from that location. Only local file domains are allowed (so you can't delete from a file repository or unpin from ipfs yet), but if you specific 'all local files', you should be able to trigger a physical delete if you wish. +If you specify a file service, the file will only be deleted from that location. Only local file domains are allowed (so you can't delete from a file repository or unpin from ipfs yet). It defaults to 'all my files', which will delete from all local services (i.e. force sending to trash). Sending 'all local files' on a file already in the trash will trigger a physical file delete. ### **POST `/add_files/undelete_files`** { id="add_files_undelete_files" } @@ -428,12 +526,8 @@ Required Headers: Arguments (in JSON): : -* `hash`: (an SHA256 hash for a file in 64 characters of hexadecimal) -* `hashes`: (a list of SHA256 hashes) -* `file_id`: (a numerical file id) -* `file_ids`: (a list of numerical file ids) -* `file_service_name`: (optional, selective, string, the local file domain to which to undelete) -* `file_service_key`: (optional, selective, hexadecimal, the local file domain to which to undelete) +* [files](#parameters_files) +* [file domain](#parameters_file_domain) (optional, defaults to 'all my files') ```json title="Example request body" {"hash" : "78f92ba4a786225ee2a1236efa6b7dc81dd729faf4af99f96f3e20bad6d8b538"} @@ -444,7 +538,7 @@ Response: You can use hash or hashes, whichever is more convenient. -This is the reverse of a delete_files--removing files from trash and putting them back where they came from. If you specify a file service, the files will only be undeleted to there (if they have a delete record, otherwise this is nullipotent). If you do not specify a file service, they will be undeleted to all local file services for which there are deletion records. There is no error if any files do not currently exist in 'trash'. +This is the reverse of a delete_files--removing files from trash and putting them back where they came from. If you specify a file service, the files will only be undeleted to there (if they have a delete record, otherwise this is nullipotent). The default, 'all my files', undeletes to all local file services for which there are deletion records. There is no error if any of the files do not currently exist in 'trash'. ### **POST `/add_files/archive_files`** { id="add_files_archive_files" } @@ -460,10 +554,7 @@ Required Headers: Arguments (in JSON): : -* `hash`: (an SHA256 hash for a file in 64 characters of hexadecimal) -* `hashes`: (a list of SHA256 hashes) -* `file_id`: (a numerical file id) -* `file_ids`: (a list of numerical file ids) +* [files](#parameters_files) ```json title="Example request body" {"hash" : "78f92ba4a786225ee2a1236efa6b7dc81dd729faf4af99f96f3e20bad6d8b538"} @@ -472,8 +563,6 @@ Arguments (in JSON): Response: : 200 and no content. -You can use hash or hashes, whichever is more convenient. - This puts files in the 'archive', taking them out of the inbox. It only has meaning for files currently in 'my files' or 'trash'. There is no error if any files do not currently exist or are already in the archive. @@ -490,10 +579,7 @@ Required Headers: Arguments (in JSON): : -* `hash`: (an SHA256 hash for a file in 64 characters of hexadecimal) -* `hashes`: (a list of SHA256 hashes) -* `file_id`: (a numerical file id) -* `file_ids`: (a list of numerical file ids) +* [files](#parameters_files) ```json title="Example request body" {"hash" : "78f92ba4a786225ee2a1236efa6b7dc81dd729faf4af99f96f3e20bad6d8b538"} @@ -502,8 +588,6 @@ Arguments (in JSON): Response: : 200 and no content. -You can use hash or hashes, whichever is more convenient. - This puts files back in the inbox, taking them out of the archive. It only has meaning for files currently in 'my files' or 'trash'. There is no error if any files do not currently exist or are already in the inbox. @@ -616,10 +700,8 @@ Arguments (in JSON): * `destination_page_key`: (optional page identifier for the page to receive the url) * `destination_page_name`: (optional page name to receive the url) * `show_destination_page`: (optional, defaulting to false, controls whether the UI will change pages on add) - * `service_names_to_additional_tags`: (optional, selective, tags to give to any files imported from this url) * `service_keys_to_additional_tags`: (optional, selective, tags to give to any files imported from this url) * `filterable_tags`: (optional tags to be filtered by any tag import options that applies to the URL) - * _`service_names_to_tags`: (obsolete, legacy synonym for service\_names\_to\_additional_tags)_ If you specify a `destination_page_name` and an appropriate importer page already exists with that name, that page will be used. Otherwise, a new page with that name will be recreated (and used by subsequent calls with that name). Make sure it that page name is unique (e.g. '/b/ threads', not 'watcher') in your client, or it may not be found. @@ -627,7 +709,7 @@ Alternately, `destination_page_key` defines exactly which page should be used. B `show_destination_page` defaults to False to reduce flicker when adding many URLs to different pages quickly. If you turn it on, the client will behave like a URL drag and drop and select the final page the URL ends up on. -`service_names_to_additional_tags` and `service_keys_to_additional_tags` use the same data structure as in /add\_tags/add\_tags--service ids to a list of tags to add. You will need 'add tags' permission or this will 403. These tags work exactly as 'additional' tags work in a _tag import options_. They are service specific, and always added unless some advanced tag import options checkbox (like 'only add tags to new files') is set. +`service_keys_to_additional_tags` uses the same data structure as in /add\_tags/add\_tags--service keys to a list of tags to add. You will need 'add tags' permission or this will 403. These tags work exactly as 'additional' tags work in a _tag import options_. They are service specific, and always added unless some advanced tag import options checkbox (like 'only add tags to new files') is set. filterable_tags works like the tags parsed by a hydrus downloader. It is just a list of strings. They have no inherant service and will be sent to a _tag import options_, if one exists, to decide which tag services get what. This parameter is useful if you are pulling all a URL's tags outside of hydrus and want to have them processed like any other downloader, rather than figuring out service names and namespace filtering on your end. Note that in order for a tag import options to kick in, I think you will have to have a Post URL URL Class hydrus-side set up for the URL so some tag import options (whether that is Class-specific or just the default) can be loaded at import time. @@ -635,8 +717,8 @@ filterable_tags works like the tags parsed by a hydrus downloader. It is just a { "url" : "https://8ch.net/tv/res/1846574.html", "destination_page_name" : "kino zone", - "service_names_to_additional_tags" : { - "my tags" : ["as seen on /tv/"] + "service_keys_to_additional_tags" : { + "6c6f63616c2074616773" : ["as seen on /tv/"] } } ``` @@ -703,16 +785,13 @@ Required Headers: Arguments (in JSON): : - * `url_to_add`: (an url you want to associate with the file(s)) - * `urls_to_add`: (a list of urls you want to associate with the file(s)) - * `url_to_delete`: (an url you want to disassociate from the file(s)) - * `urls_to_delete`: (a list of urls you want to disassociate from the file(s)) - * `hash`: (an SHA256 hash for a file in 64 characters of hexadecimal) - * `hashes`: (a list of SHA256 hashes) - * `file_id`: (a numerical file id) - * `file_ids`: (a list of numerical file ids) + * `url_to_add`: (optional, selective A, an url you want to associate with the file(s)) + * `urls_to_add`: (optional, selective A, a list of urls you want to associate with the file(s)) + * `url_to_delete`: (optional, selective B, an url you want to disassociate from the file(s)) + * `urls_to_delete`: (optional, selective B, a list of urls you want to disassociate from the file(s)) + * [files](#parameters_files) - All of these are optional, but you obviously need to have at least one of `url` arguments and one of the `hash` arguments. The single/multiple arguments work the same--just use whatever is convenient for you. Unless you really know what you are doing with URL Classes, I strongly recommend you stick to associating URLs with just one single 'hash' at a time. Multiple hashes pointing to the same URL is unusual and frequently unhelpful. + The single/multiple arguments work the same--just use whatever is convenient for you. Unless you really know what you are doing with URL Classes, I strongly recommend you stick to associating URLs with just one single 'hash' at a time. Multiple hashes pointing to the same URL is unusual and frequently unhelpful. ```json title="Example request body" { "url_to_add" : "https://rule34.xxx/index.php?id=2588418&page=post&s=view", @@ -756,32 +835,6 @@ Response: Mostly, hydrus simply trims excess whitespace, but the other examples are rare issues you might run into. 'system' is an invalid namespace, tags cannot be prefixed with hyphens, and any tag starting with ':' is secretly dealt with internally as "\[no namespace\]:\[colon-prefixed-subtag\]". Again, you probably won't run into these, but if you see a mismatch somewhere and want to figure it out, or just want to sort some numbered tags, you might like to try this. -### **GET `/add_tags/get_tag_services`** { id="add_tags_get_tag_services" } - -!!! warning "Deprecated" - This is becoming obsolete and will be removed! Use [/get_services](#get_services) instead! - -_Ask the client about its tag services._ - -Restricted access: -: YES. Add Tags permission needed. - -Required Headers: n/a - -Arguments: n/a - -Response: -: Some JSON listing the client's 'local tags' and tag repository services by name. -```json title="Example response" -{ - "local_tags" : ["my tags"], - "tag_repositories" : [ "public tag repository", "mlp fanfic tagging server" ] -} -``` - - !!! note - A user can rename their services. Don't assume the client's local tags service will be "my tags". - ### **GET `/add_tags/search_tags`** { id="add_tags_search_tags" } _Search the client for tags._ @@ -793,15 +846,21 @@ Required Headers: n/a Arguments: : -* `search`: (the tag text to search for, enter exactly what you would in the client UI) -* `tag_service_key`: (optional, selective, hexadecimal, the tag domain on which to search) -* `tag_service_name`: (optional, selective, string, the tag domain on which to search) -* `tag_display_type`: (optional, string, to select whether to search raw or sibling-processed tags) + * `search`: (the tag text to search for, enter exactly what you would in the client UI) + * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') + * `tag_service_key`: (optional, hexadecimal, the tag domain on which to search, defaults to 'all known tags') + * `tag_display_type`: (optional, string, to select whether to search raw or sibling-processed tags, defaults to 'storage') + +The `file domain` and `tag_service_key` perform the function of the file and tag domain buttons in the client UI. + +The `tag_display_type` can be either `storage` (the default), which searches your file's stored tags, just as they appear in a 'manage tags' dialog, or `display`, which searches the sibling-processed tags, just as they appear in a normal file search page. In the example above, setting the `tag_display_type` to `display` could well combine the two kim possible tags and give a count of 3 or 4. + +'all my files'/'all known tags' works fine for most cases, but a specific tag service or 'all known files'/'tag service' can work better for editing tag repository `storage` contexts, since it provides results just for that service, and for repositories, it gives tags for all the non-local files other users have tagged. Example request: : ```http title="Example request" -/add_tags/search_tags?search=kim +/add_tags/search_tags?search=kim&tag_display_type=display ``` Response: @@ -827,14 +886,10 @@ Response: } ``` -The `tags` list will be sorted by descending count. If you do not specify a tag service, it will default to 'all known tags'. The various rules in _tags->manage tag display and search_ (e.g. no pure `*` searches on certain services) will also be checked--and if violated, you will get 200 OK but an empty result. - -The `tag_display_type` can be either `storage` (the default), which searches your file's stored tags, just as they appear in a 'manage tags' dialog, or `display`, which searches the sibling-processed tags, just as they appear in a normal file search page. In the example above, setting the `tag_display_type` to `display` could well combine the two kim possible tags and give a count of 3 or 4. +The `tags` list will be sorted by descending count. The various rules in _tags->manage tag display and search_ (e.g. no pure `*` searches on certain services) will also be checked--and if violated, you will get 200 OK but an empty result. Note that if your client api access is only allowed to search certain tags, the results will be similarly filtered. -Also, for now, it gives you the 'storage' tags, which are the 'raw' ones you see in the manage tags dialog, without collapsed siblings, but more options will be added in future. - ### **POST `/add_tags/add_tags`** { id="add_tags_add_tags" } _Make changes to the tags that files have._ @@ -846,21 +901,14 @@ Required Headers: n/a Arguments (in JSON): : -* `hash`: (selective A, an SHA256 hash for a file in 64 characters of hexadecimal) -* `hashes`: (selective A, a list of SHA256 hashes) -* `file_id`: (a numerical file id) -* `file_ids`: (a list of numerical file ids) -* `service_names_to_tags`: (selective B, an Object of service names to lists of tags to be 'added' to the files) +* [files](#parameters_files) * `service_keys_to_tags`: (selective B, an Object of service keys to lists of tags to be 'added' to the files) -* `service_names_to_actions_to_tags`: (selective B, an Object of service names to content update actions to lists of tags) * `service_keys_to_actions_to_tags`: (selective B, an Object of service keys to content update actions to lists of tags) - You can use either 'hash' or 'hashes'. - - You can use either 'service\_names\_to...' or 'service\_keys\_to...', where names is simple and human-friendly "my tags" and similar (but may be renamed by a user), but keys is a little more complicated but accurate/unique. Since a client may have multiple tag services with non-default names and pseudo-random keys, if it is not your client you will need to check the [/get_services](#get_services) call to get the names or keys, and you may need some selection UI on your end so the user can pick what to do if there are multiple choices. I encourage using keys if you can. - + In 'service\_keys\_to...', the keys are as in [/get\_services](#get_services). You may need some selection UI on your end so the user can pick what to do if there are multiple choices. + Also, you can use either '...to\_tags', which is simple and add-only, or '...to\_actions\_to\_tags', which is more complicated and allows you to remove/petition or rescind pending content. - + The permitted 'actions' are: * 0 - Add to a local tag service. @@ -877,8 +925,8 @@ Some example requests: ```json title="Adding some tags to a file" { "hash" : "df2a7b286d21329fc496e3aa8b8a08b67bb1747ca32749acb3f5d544cbfc0f56", - "service_names_to_tags" : { - "my tags" : ["character:supergirl", "rating:safe"] + "service_keys_to_tags" : { + "6c6f63616c2074616773" : ["character:supergirl", "rating:safe"] } } ``` @@ -888,9 +936,9 @@ Some example requests: "df2a7b286d21329fc496e3aa8b8a08b67bb1747ca32749acb3f5d544cbfc0f56", "f2b022214e711e9a11e2fcec71bfd524f10f0be40c250737a7861a5ddd3faebf" ], - "service_names_to_tags" : { - "my tags" : ["process this"], - "public tag repository" : ["creator:dandon fuga"] + "service_keys_to_tags" : { + "6c6f63616c2074616773" : ["process this"], + "ccb0cf2f9e92c2eb5bd40986f72a339ef9497014a5fb8ce4cea6d6c9837877d9" : ["creator:dandon fuga"] } } ``` @@ -914,7 +962,7 @@ Some example requests: This last example is far more complicated than you will usually see. Pend rescinds and petition rescinds are not common. Petitions are also quite rare, and gathering a good petition reason for each tag is often a pain. - Note that the enumerated status keys in the service\_names\_to\_actions\_to_tags structure are strings, not ints (JSON does not support int keys for Objects). + Note that the enumerated status keys in the service\_keys\_to\_actions\_to_tags structure are strings, not ints (JSON does not support int keys for Objects). Response description: : 200 and no content. @@ -1028,16 +1076,12 @@ Required Headers: n/a Arguments (in percent-encoded JSON): : * `tags`: (a list of tags you wish to search for) - * `file_service_name`: (optional, selective, string, the file domain on which to search) - * `file_service_key`: (optional, selective, hexadecimal, the file domain on which to search) - * `tag_service_name`: (optional, selective, string, the tag domain on which to search) - * `tag_service_key`: (optional, selective, hexadecimal, the tag domain on which to search) - * `file_sort_type`: (optional, integer, the results sort method) + * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') + * `tag_service_key`: (optional, hexadecimal, the tag domain on which to search, defaults to 'all my files') + * `file_sort_type`: (optional, integer, the results sort method, defaults to 'all known tags') * `file_sort_asc`: true or false (optional, the results sort order) * `return_file_ids`: true or false (optional, default true, returns file id results) * `return_hashes`: true or false (optional, default false, returns hex hash results) - * _`system_inbox`: true or false (obsolete, use tags)_ - * _`system_archive`: true or false (obsolete, use tags)_ ``` title='Example request for 16 files (system:limit=16) in the inbox with tags "blue eyes", "blonde hair", and "кино"' /get_files/search_files?tags=%5B%22blue%20eyes%22%2C%20%22blonde%20hair%22%2C%20%22%5Cu043a%5Cu0438%5Cu043d%5Cu043e%22%2C%20%22system%3Ainbox%22%2C%20%22system%3Alimit%3D16%22%5D @@ -1046,8 +1090,6 @@ Arguments (in percent-encoded JSON): If the access key's permissions only permit search for certain tags, at least one positive whitelisted/non-blacklisted tag must be in the "tags" list or this will 403. Tags can be prepended with a hyphen to make a negated tag (e.g. "-green eyes"), but these will not be checked against the permissions whitelist. -File searches occur in the `display` `tag_display_type`. If you want to pair autocomplete tag lookup from [/search_tags](#add_tags_search_tags) to this file search (e.g. for making a standard booru search interface), then make sure you are searching `display` tags there. - Wildcards and namespace searches are supported, so if you search for 'character:sam*' or 'series:*', this will be handled correctly clientside. **Many system predicates are also supported using a text parser!** The parser was designed by a clever user for human input and allows for a certain amount of error (e.g. ~= instead of ≈, or "isn't" instead of "is not") or requires more information (e.g. the specific hashes for a hash lookup). **Here's a big list of examples that are supported:** @@ -1109,8 +1151,10 @@ Wildcards and namespace searches are supported, so if you search for 'character: * system:file service currently in my files * system:file service is not currently in my files * system:file service is not pending to my files + * system:number of file relationships = 2 duplicates + * system:number of file relationships > 10 potential duplicates * system:num file relationships < 3 alternates - * system:number of file relationships > 3 false positives + * system:num file relationships > 3 false positives * system:ratio is wider than 16:9 * system:ratio is 16:9 * system:ratio taller than 1:1 @@ -1153,7 +1197,9 @@ Makes: * samus aran OR lara croft * system:height > 1000 -The file and tag services are for search domain selection, just like clicking the buttons in the client. They are optional--default is 'my files' and 'all known tags', and you can use either key or name as in [GET /get_services](#get_services), whichever is easiest for your situation. +The file and tag services are for search domain selection, just like clicking the buttons in the client. They are optional--default is 'all my files' and 'all known tags'. + +File searches occur in the `display` `tag_display_type`. If you want to pair autocomplete tag lookup from [/search_tags](#add_tags_search_tags) to this file search (e.g. for making a standard booru search interface), then make sure you are searching `display` tags there. file\_sort\_asc is 'true' for ascending, and 'false' for descending. The default is descending. @@ -1242,24 +1288,20 @@ _Get metadata about files in the client._ Restricted access: : YES. Search for Files permission needed. Additional search permission limits may apply. - + Required Headers: n/a - + Arguments (in percent-encoded JSON): : - * `file_id`: (selective, a numerical file id) - * `file_ids`: (selective, a list of numerical file ids) - * `hash`: (selective, a hexadecimal SHA256 hash) - * `hashes`: (selective, a list of hexadecimal SHA256 hashes) + * [files](#parameters_files) * `create_new_file_ids`: true or false (optional if asking with hash(es), defaulting to false) * `only_return_identifiers`: true or false (optional, defaulting to false) * `only_return_basic_information`: true or false (optional, defaulting to false) * `detailed_url_information`: true or false (optional, defaulting to false) * `include_notes`: true or false (optional, defaulting to false) - * `hide_service_keys_tags`: **Will be set default false and deprecated soon!** true or false (optional, defaulting to false) - * `hide_service_names_tags`: **Deprecated, will be deleted soon!** true or false (optional, defaulting to true) + * `hide_service_keys_tags`: **Deprecated, will be deleted soon!** true or false (optional, defaulting to true) -You need one of file_ids or hashes. If your access key is restricted by tag, you cannot search by hashes, and **the file_ids you search for must have been in the most recent search result**. +If your access key is restricted by tag, **the files you search for must have been in the most recent search result**. ``` title="Example request for two files with ids 123 and 4567" /get_files/file_metadata?file_ids=%5B123%2C%204567%5D @@ -1309,8 +1351,6 @@ Response: "has_human_readable_embedded_metadata" : true, "has_icc_profile" : true, "known_urls" : [], - "service_keys_to_statuses_to_tags" : {}, - "service_keys_to_statuses_to_display_tags" : {}, "tags" : { "6c6f63616c2074616773" : { "name" : "local tags", @@ -1400,34 +1440,6 @@ Response: "https://img2.gelbooru.com//images/80/c8/80c8646b4a49395fb36c805f316c49a9.jpg", "http://origin-orig.deviantart.net/ed31/f/2019/210/7/8/beachqueen_samus_by_dandonfuga-ddcu1xg.jpg" ], - "service_keys_to_statuses_to_tags" : { - "6c6f63616c2074616773" : { - "0" : ["samus favourites"], - "2" : ["process this later"] - }, - "37e3849bda234f53b0e9792a036d14d4f3a9a136d1cb939705dbcd5287941db4" : { - "0" : ["blonde_hair", "blue_eyes", "looking_at_viewer"], - "1" : ["bodysuit"] - }, - "616c6c206b6e6f776e2074616773" : { - "0" : ["samus favourites", "blonde_hair", "blue_eyes", "looking_at_viewer"], - "1" : ["bodysuit"] - } - }, - "service_keys_to_statuses_to_display_tags" : { - "6c6f63616c2074616773" : { - "0" : ["samus favourites", "favourites"], - "2" : ["process this later"] - }, - "37e3849bda234f53b0e9792a036d14d4f3a9a136d1cb939705dbcd5287941db4" : { - "0" : ["blonde hair", "blue_eyes", "looking at viewer"], - "1" : ["bodysuit", "clothing"] - }, - "616c6c206b6e6f776e2074616773" : { - "0" : ["samus favourites", "favourites", "blonde hair", "blue_eyes", "looking at viewer"], - "1" : ["bodysuit", "clothing"] - } - }, "tags" : { "6c6f63616c2074616773" : { "name" : "local tags", @@ -1530,17 +1542,15 @@ Size is in bytes. Duration is in milliseconds, and may be an int or a float. `ipfs_multihashes` stores the ipfs service key to any known multihash for the file. -The `thumbnail_width` and `thumbnail_height` are a generally reliable prediction but aren't a promise. The actual thumbnail you get from [/get_files/thumbnail](#get_files_thumbnail) will be different if the user hasn't looked at it since changing their thumbnail options. You only get these rows for files that hydrus actually generates an actual thumbnail for. Things like pdf won't have it. You can use your own thumb, or ask the api and it'll give you a fixed fallback; those are mostly 200x200, but you can and should size them to whatever you want. +The `thumbnail_width` and `thumbnail_height` are a generally reliable prediction but aren't a promise. The actual thumbnail you get from [/get\_files/thumbnail](#get_files_thumbnail) will be different if the user hasn't looked at it since changing their thumbnail options. You only get these rows for files that hydrus actually generates an actual thumbnail for. Things like pdf won't have it. You can use your own thumb, or ask the api and it'll give you a fixed fallback; those are mostly 200x200, but you can and should size them to whatever you want. #### tags The 'tags' structures are undergoing transition. Previously, this was a mess of different Objects in different domains, all `service_xxx_to_xxx_tags`, but they are being transitioned to the combined `tags` Object. -`hide_service_names_tags` is deprecated and will be deleted soon. When set to `false`, it shows the old `service_names_to_statuses_to_tags` and `service_names_to_statuses_to_display_tags` Objects. The new `tags` structure now shows the service name--migrate to this asap. +`hide_service_keys_tags` is deprecated and will be deleted soon. When set to `false`, it shows the old `service_keys_to_statuses_to_tags` and `service_keys_to_statuses_to_display_tags` Objects. -`hide_service_keys_tags` will soon be set to default `false` and deprecated in the same way. Move to `tags` please! - -The `tags` structures are similar to the [/add_tags/add_tags](#add_tags_add_tags) scheme, excepting that the status numbers are: +The `tags` structures are similar to the [/add\_tags/add\_tags](#add_tags_add_tags) scheme, excepting that the status numbers are: * 0 - current * 1 - pending @@ -1550,7 +1560,7 @@ The `tags` structures are similar to the [/add_tags/add_tags](#add_tags_add_tags !!! note Since JSON Object keys must be strings, these status numbers are strings, not ints. -To learn more about service names and keys on a client, use the [/get_services](#get_services) call. +To learn more about service names and keys on a client, use the [/get\_services](#get_services) call. While the 'storage_tags' represent the actual tags stored on the database for a file, 'display_tags' reflect how tags appear in the UI, after siblings are collapsed and parents are added. If you want to edit a file's tags, refer to the storage tags. If you want to render to the user, use the display tags. The display tag calculation logic is very complicated; if the storage tags change, do not try to guess the new display tags yourself--just ask the API again. @@ -1665,7 +1675,7 @@ This refers to the File Relationships system, which includes 'potential duplicat This system is pending significant rework and expansion, so please do not get too married to some of the routines here. I am mostly just exposing my internal commands, so things are a little ugly/hacked. I expect duplicate and alternate groups to get some form of official identifier in future, which may end up being the way to refer and edit things here. -Also, at least for now, 'Manage File Relationships' permission is not going to be bound by the search permission restrictions that normal file search does. Getting this permission allows you to search anything. I expect to add this permission filtering tech in future, particularly for file domains. +Also, at least for now, 'Manage File Relationships' permission is not going to be bound by the search permission restrictions that normal file search does. Getting this file relationship management permission allows you to search anything. _There is more work to do here, including adding various 'dissolve'/'undo' commands to break groups apart._ @@ -1680,10 +1690,8 @@ Required Headers: n/a Arguments (in percent-encoded JSON): : - * `file_id`: (selective, a numerical file id) - * `file_ids`: (selective, a list of numerical file ids) - * `hash`: (selective, a hexadecimal SHA256 hash) - * `hashes`: (selective, a list of hexadecimal SHA256 hashes) + * [files](#parameters_files) + * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') ``` title="Example request" /manage_file_relationships/get_file_relationships?hash=ac940bb9026c430ea9530b4f4f6980a12d9432c2af8d9d39dfc67b05d91df11d @@ -1714,7 +1722,9 @@ Response: `is_king` and `king` relate to which file is the set best of a group. The king is usually the best representative of a group if you need to do comparisons between groups, and the 'get some pairs to filter'-style commands usually try to select the kings of the various to-be-compared duplicate groups. -**It is possible for the king to not be available, in which case `king` is null.** The king can be unavailable in several duplicate search contexts, generally when you have the option to search/filter and it is outside of that domain. For this request, the king will usually be available unless the user has deleted it. You have to deal with the king being unavailable--in this situation, your best bet is to just use the file itself as its own representative. +The relationships you get are filtered by the file domain. If you set the file domain to 'all known files', you will get every relationship a file has, including all deleted files, which is often less useful than you would think. The default, 'all my files' is usually most useful. + +**It is possible for the king to not be available, in which case `king` is null.** The king can be unavailable in several duplicate search contexts, generally when it is outside of the set file domain. For the default domain, 'all my files', the king will be available unless the user has deleted it. You have to deal with the king being unavailable--in this situation, your best bet is to just use the file itself as its own representative. A file that has no duplicates is considered to be in a duplicate group of size 1 and thus is always its own king. @@ -1740,6 +1750,7 @@ Required Headers: n/a Arguments (in percent-encoded JSON): : + * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') * `tag_service_key_1`: (optional, default 'all known tags', a hex tag service key) * `tags_1`: (optional, default system:everything, a list of tags you wish to search for) * `tag_service_key_2`: (optional, default 'all known tags', a hex tag service key) @@ -1749,10 +1760,10 @@ Arguments (in percent-encoded JSON): * `max_hamming_distance`: (optional, integer, default 4, the max 'search distance' of the pairs) ``` title="Example request" -/manage_file_relationships/get_potentials_count?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&search_type=1&pixel_duplicates=2&max_hamming_distance=0&max_num_pairs=50 +/manage_file_relationships/get_potentials_count?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&potentials_search_type=1&pixel_duplicates=2&max_hamming_distance=0&max_num_pairs=50 ``` -`tag_service_key` and `tags` work the same as [/get\_files/search\_files](#get_files_search_files). The `_2` variants are only useful if the `potentials_search_type` is 2. For now the file domain is locked to 'all my files'. +`tag_service_key_x` and `tags_x` work the same as [/get\_files/search\_files](#get_files_search_files). The `_2` variants are only useful if the `potentials_search_type` is 2. `potentials_search_type` and `pixel_duplicates` are enums: @@ -1789,6 +1800,7 @@ Required Headers: n/a Arguments (in percent-encoded JSON): : + * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') * `tag_service_key_1`: (optional, default 'all known tags', a hex tag service key) * `tags_1`: (optional, default system:everything, a list of tags you wish to search for) * `tag_service_key_2`: (optional, default 'all known tags', a hex tag service key) @@ -1799,7 +1811,7 @@ Arguments (in percent-encoded JSON): * `max_num_pairs`: (optional, integer, defaults to client's option, how many pairs to get in a batch) ``` title="Example request" -/manage_file_relationships/get_potential_pairs?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&search_type=1&pixel_duplicates=2&max_hamming_distance=0&max_num_pairs=50 +/manage_file_relationships/get_potential_pairs?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&potentials_search_type=1&pixel_duplicates=2&max_hamming_distance=0&max_num_pairs=50 ``` The search arguments work the same as [/manage\_file\_relationships/get\_potentials\_count](#manage_file_relationships_get_potentials_count). @@ -1833,6 +1845,7 @@ Required Headers: n/a Arguments (in percent-encoded JSON): : + * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') * `tag_service_key_1`: (optional, default 'all known tags', a hex tag service key) * `tags_1`: (optional, default system:everything, a list of tags you wish to search for) * `tag_service_key_2`: (optional, default 'all known tags', a hex tag service key) @@ -1842,7 +1855,7 @@ Arguments (in percent-encoded JSON): * `max_hamming_distance`: (optional, integer, default 4, the max 'search distance' of the files) ``` title="Example request" -/manage_file_relationships/get_random_potentials?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&search_type=1&pixel_duplicates=2&max_hamming_distance=0 +/manage_file_relationships/get_random_potentials?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&potentials_search_type=1&pixel_duplicates=2&max_hamming_distance=0 ``` The arguments work the same as [/manage\_file\_relationships/get\_potentials\_count](#manage_file_relationships_get_potentials_count), with the caveat that `potentials_search_type` has special logic: @@ -1881,13 +1894,20 @@ Required Headers: Arguments (in JSON): : - * `pair_rows`: (a list of lists) + * `relationships`: (a list of Objects, one for each file-pair being set) -Each row is: +Each Object is: - * [ relationship, hash_a, hash_b, do_default_content_merge, delete_a, delete_b ] + * `hash_a`: (a hexadecimal SHA256 hash) + * `hash_b`: (a hexadecimal SHA256 hash) + * `relationship`: (integer enum for the relationship being set) + * `do_default_content_merge`: (bool) + * `delete_a`: (optional, bool, default false) + * `delete_b`: (optional, bool, default false) -Where `relationship` is one of this enum: +`hash_a` and `hash_b` are normal hex SHA256 hashes for your file pair. + +`relationship` is one of this enum: * 0 - set as potential duplicates * 1 - set as false positives @@ -1898,18 +1918,33 @@ Where `relationship` is one of this enum: 2, 4, and 7 all make the files 'duplicates' (8 under `get_file_relationships`), which, specifically, merges the two files' duplicate groups. 'same quality' has different duplicate content merge options to the better/worse choices, but it ultimately sets A>B. You obviously don't have to use 'B is better' if you prefer just to swap the hashes. Do what works for you. -`hash_a` and `hash_b` are normal hex SHA256 hashes for your file pair. +`do_default_content_merge` sets whether the user's duplicate content merge options should be loaded and applied to the files along with the relationship. Most operations in the client do this automatically, so the user may expect it to apply, but if you want to do content merge yourself, set this to false. -`do_default_content_merge` is a boolean setting whether the user's duplicate content merge options should be loaded and applied to the files along with the duplicate status. Most operations in the client do this automatically, so the user may expect it to apply, but if you want to do content merge yourself, set this to false. - -`delete_a` and `delete_b` are booleans that obviously select whether to delete A and/or B. You can also do this externally if you prefer. +`delete_a` and `delete_b` are booleans that select whether to delete A and/or B in the same operation as setting the relationship. You can also do this externally if you prefer. ```json title="Example request body" { - "pair_rows" : [ - [ 4, "b54d09218e0d6efc964b78b070620a1fa19c7e069672b4c6313cee2c9b0623f2", "bbaa9876dab238dcf5799bfd8319ed0bab805e844f45cf0de33f40697b11a845", true, false, true ], - [ 4, "22667427eaa221e2bd7ef405e1d2983846c863d40b2999ce8d1bf5f0c18f5fb2", "65d228adfa722f3cd0363853a191898abe8bf92d9a514c6c7f3c89cfed0bf423", true, false, true ], - [ 2, "0480513ffec391b77ad8c4e57fe80e5b710adfa3cb6af19b02a0bd7920f2d3ec", "5fab162576617b5c3fc8caabea53ce3ab1a3c8e0a16c16ae7b4e4a21eab168a7", true, false, false ] + "relationships" : [ + { + "hash_a" : "b54d09218e0d6efc964b78b070620a1fa19c7e069672b4c6313cee2c9b0623f2", + "hash_b" : "bbaa9876dab238dcf5799bfd8319ed0bab805e844f45cf0de33f40697b11a845", + "relationship" : 4, + "do_default_content_merge" : true, + "delete_b" : true + }, + { + "hash_a" : "22667427eaa221e2bd7ef405e1d2983846c863d40b2999ce8d1bf5f0c18f5fb2", + "hash_b" : "65d228adfa722f3cd0363853a191898abe8bf92d9a514c6c7f3c89cfed0bf423", + "relationship" : 4, + "do_default_content_merge" : true, + "delete_b" : true + }, + { + "hash_a" : "0480513ffec391b77ad8c4e57fe80e5b710adfa3cb6af19b02a0bd7920f2d3ec", + "hash_b" : "5fab162576617b5c3fc8caabea53ce3ab1a3c8e0a16c16ae7b4e4a21eab168a7", + "relationship" : 2, + "do_default_content_merge" : true + } ] } ``` @@ -1917,7 +1952,7 @@ Where `relationship` is one of this enum: Response: : 200 with no content. -If you try to add an invalid or redundant relationship, for instance setting that files that are already duplicates are potential duplicates, no changes are made. +If you try to add an invalid or redundant relationship, for instance setting files that are already duplicates as potential duplicates, no changes are made. This is the file relationships request that is probably most likely to change in future. I may implement content merge options. I may move from file pairs to group identifiers. When I expand alternates, those file groups are going to support more variables. @@ -1934,10 +1969,7 @@ Required Headers: Arguments (in JSON): : - * `file_id`: (selective, a numerical file id) - * `file_ids`: (selective, a list of numerical file ids) - * `hash`: (selective, a hexadecimal SHA256 hash) - * `hashes`: (selective, a list of hexadecimal SHA256 hashes) + * [files](#parameters_files) ```json title="Example request body" { @@ -2061,36 +2093,42 @@ Response: "pages" : { "name" : "top pages notebook", "page_key" : "3b28d8a59ec61834325eb6275d9df012860a1ecfd9e1246423059bc47fb6d5bd", + "page_state" : 0, "page_type" : 10, "selected" : true, "pages" : [ { "name" : "files", "page_key" : "d436ff5109215199913705eb9a7669d8a6b67c52e41c3b42904db083255ca84d", + "page_state" : 0, "page_type" : 6, "selected" : false }, { "name" : "thread watcher", "page_key" : "40887fa327edca01e1d69b533dddba4681b2c43e0b4ebee0576177852e8c32e7", + "page_state" : 0, "page_type" : 9, "selected" : false }, { "name" : "pages", "page_key" : "2ee7fa4058e1e23f2bd9e915cdf9347ae90902a8622d6559ba019a83a785c4dc", + "page_state" : 0, "page_type" : 10, "selected" : true, "pages" : [ { "name" : "urls", "page_key" : "9fe22cb760d9ee6de32575ed9f27b76b4c215179cf843d3f9044efeeca98411f", + "page_state" : 0, "page_type" : 7, "selected" : true }, { "name" : "files", "page_key" : "2977d57fc9c588be783727bcd54225d577b44e8aa2f91e365a3eb3c3f580dc4e", + "page_state" : 0, "page_type" : 6, "selected" : false } @@ -2101,7 +2139,11 @@ Response: } ``` - The page types are as follows: + `name` is the full text on the page tab. + + `page_key` is a unique identifier for the page. It will stay the same for a particular page throughout the session, but new ones are generated on a session reload. + + `page_type` is as follows: * 1 - Gallery downloader * 2 - Simple downloader @@ -2112,10 +2154,20 @@ Response: * 8 - Duplicates * 9 - Thread watcher * 10 - Page of pages - - The top page of pages will always be there, and always selected. 'selected' means which page is currently in view and will propagate down other page of pages until it terminates. It may terminate in an empty page of pages, so do not assume it will end on a 'media' page. - - The 'page_key' is a unique identifier for the page. It will stay the same for a particular page throughout the session, but new ones are generated on a client restart or other session reload. + + `page_state` is as follows: + + * 0 - ready + * 1 - initialising + * 2 - searching/loading + * 3 - search cancelled + + Most pages will be 0, normal/ready, at all times. Large pages will start in an 'initialising' state for a few seconds, which means their session-saved thumbnails aren't loaded yet. Search pages will enter 'searching' after a refresh or search change and will either return to 'ready' when the search is complete, or fall to 'search cancelled' if the search was interrupted (usually this means the user clicked the 'stop' button that appears after some time). + + `selected` means which page is currently in view. It will propagate down the page of pages until it terminates. It may terminate in an empty page of pages, so do not assume it will end on a media page. + + The top page of pages will always be there, and always selected. + ### **GET `/manage_pages/get_page_info`** { id="manage_pages_get_page_info" } @@ -2145,6 +2197,7 @@ Response description "page_info" : { "name" : "threads", "page_key" : "aebbf4b594e6986bddf1eeb0b5846a1e6bc4e07088e517aff166f1aeb1c3c9da", + "page_state" : 0, "page_type" : 3, "management" : { "multiple_watcher_import" : { @@ -2206,6 +2259,8 @@ Response description } ``` + `name`, `page_key`, `page_state`, and `page_type` are as in [/manage\_pages/get\_pages](#manage_pages_get_pages). + As you can see, even the 'simple' mode can get very large. Imagine that response for a page watching 100 threads! Turning simple mode off will display every import item, gallery log entry, and all hashes in the media (thumbnail) panel. For this first version, the five importer pages--hdd import, simple downloader, url downloader, gallery page, and watcher page--all give rich info based on their specific variables. The first three only have one importer/gallery log combo, but the latter two of course can have multiple. The "imports" and "gallery_log" entries are all in the same data format. @@ -2225,10 +2280,7 @@ Required Headers: Arguments (in JSON): : * `page_key`: (the page key for the page you wish to add files to) - * `file_id`: (selective, a numerical file id) - * `file_ids`: (selective, a list of numerical file ids) - * `hash`: (selective, a hexadecimal SHA256 hash) - * `hashes`: (selective, a list of hexadecimal SHA256 hashes) + * [files](#parameters_files) The files you set will be appended to the given page, just like a thumbnail drag and drop operation. The page key is the same as fetched in the [/manage\_pages/get\_pages](#manage_pages_get_pages) call. @@ -2295,6 +2347,8 @@ The page key is the same as fetched in the [/manage\_pages/get\_pages](#manage_p Response: : 200 with no content. If the page key is not found, this will 404. +Poll the `page_state` in [/manage\_pages/get\_pages](#manage_pages_get_pages) or [/manage\_pages/get\_page\_info](#manage_pages_get_page_info) to see when the search is complete. + ## Managing the Database ### **POST `/manage_database/lock_on`** { id="manage_database_lock_on" } diff --git a/docs/developer_api_future.md b/docs/developer_api_future.md deleted file mode 100644 index 235b0670..00000000 --- a/docs/developer_api_future.md +++ /dev/null @@ -1,2318 +0,0 @@ ---- -title: API documentation -hide: navigation ---- - -# API documentation - -## Library modules created by hydrus users - -* [Hydrus API](https://gitlab.com/cryzed/hydrus-api): A python module that talks to the API. -* [hydrus.js](https://github.com/cravxx/hydrus.js): A node.js module that talks to the API. -* [more projects on github](https://github.com/stars/hydrusnetwork/lists/hydrus-related-projects) - -## API - -In general, the API deals with standard UTF-8 JSON. POST requests and 200 OK responses are generally going to be a JSON 'Object' with variable names as keys and values obviously as values. There are examples throughout this document. For GET requests, everything is in standard GET parameters, but some variables are complicated and will need to be JSON encoded and then URL encoded. An example would be the 'tags' parameter on [GET /get\_files/search\_files](#get_files_search_files), which is a list of strings. Since GET http URLs have limits on what characters are allowed, but hydrus tags can have all sorts of characters, you'll be doing this: - -* Your list of tags: - - ``` - [ 'character:samus aran', 'creator:青い桜', 'system:height > 2000' ] - ``` - -* JSON encoded: - - ```json - ["character:samus aran", "creator:\\u9752\\u3044\\u685c", "system:height > 2000"] - ``` - -* Then URL encoded: - - ``` - %5B%22character%3Asamus%20aran%22%2C%20%22creator%3A%5Cu9752%5Cu3044%5Cu685c%22%2C%20%22system%3Aheight%20%3E%202000%22%5D - ``` - -* In python, converting your tag list to the URL encoded string would be: - - ``` - urllib.parse.quote( json.dumps( tag_list ) ) - ``` - -* Full URL path example: - - ``` - /get_files/search_files?file_sort_type=6&file_sort_asc=false&tags=%5B%22character%3Asamus%20aran%22%2C%20%22creator%3A%5Cu9752%5Cu3044%5Cu685c%22%2C%20%22system%3Aheight%20%3E%202000%22%5D - ``` - - -On 200 OK, the API returns JSON for everything except actual file/thumbnail requests. On 4XX and 5XX, assume it will return plain text, which may be a raw traceback that I'd be interested in seeing. You'll typically get 400 for a missing parameter, 401/403/419 for missing/insufficient/expired access, and 500 for a real deal serverside error. - -!!! note - For any request sent to the API, the total size of the initial request line (this includes the URL and any parameters) and the headers must not be larger than 2 megabytes. - Exceeding this limit will cause the request to fail. Make sure to use pagination if you are passing very large JSON arrays as parameters in a GET request. - - -## CBOR - -The API now tentatively supports CBOR, which is basically 'byte JSON'. If you are in a lower level language or need to do a lot of heavy work quickly, try it out! - -To send CBOR, for POST put Content-Type `application/cbor` in your request header instead of `application/json`, and for GET just add a `cbor=1` parameter to the URL string. Use CBOR to encode any parameters that you would previously put in JSON: - -For POST requests, just print the pure bytes in the body, like this: - -``` -cbor2.dumps( arg_dict ) -``` - -For GET, encode the parameter value in base64, like this: - -``` -base64.urlsafe_b64encode( cbor2.dumps( argument ) ) -``` --or- -``` -str( base64.urlsafe_b64encode( cbor2.dumps( argument ) ), 'ascii' ) -``` - -If you send CBOR, the client will return CBOR. If you want to send CBOR and get JSON back, or _vice versa_ (or you are uploading a file and can't set CBOR Content-Type), send the Accept request header, like so: - -``` -Accept: application/cbor -Accept: application/json -``` - -If the client does not support CBOR, you'll get 406. - -## Access and permissions - -The client gives access to its API through different 'access keys', which are the typical 64-character hex used in many other places across hydrus. Each guarantees different permissions such as handling files or tags. Most of the time, a user will provide full access, but do not assume this. If the access header or parameter is not provided, you will get 401, and all insufficient permission problems will return 403 with appropriate error text. - -Access is required for every request. You can provide this as an http header, like so: - -``` -Hydrus-Client-API-Access-Key : 0150d9c4f6a6d2082534a997f4588dcf0c56dffe1d03ffbf98472236112236ae -``` - -Or you can include it as a GET or POST parameter on any request (except _POST /add\_files/add\_file_, which uses the entire POST body for the file's bytes). Use the same name for your GET or POST argument, such as: - -``` -/get_files/thumbnail?file_id=452158&Hydrus-Client-API-Access-Key=0150d9c4f6a6d2082534a997f4588dcf0c56dffe1d03ffbf98472236112236ae -``` - -There is now a simple 'session' system, where you can get a temporary key that gives the same access without having to include the permanent access key in every request. You can fetch a session key with the [/session_key](#session_key) command and thereafter use it just as you would an access key, just with _Hydrus-Client-API-Session-Key_ instead. - -Session keys will expire if they are not used within 24 hours, or if the client is restarted, or if the underlying access key is deleted. An invalid/expired session key will give a **419** result with an appropriate error text. - -Bear in mind the Client API is still under construction. Setting up the Client API to be accessible across the internet requires technical experience to be convenient. HTTPS is available for encrypted comms, but the default certificate is self-signed (which basically means an eavesdropper can't see through it, but your ISP/government could if they decided to target you). If you have your own domain and SSL cert, you can replace them though (check the db directory for client.crt and client.key). Otherwise, be careful about transmitting sensitive content outside of your localhost/network. - -## Common Complex Parameters - -### **files** { id="parameters_files" } - -If you need to refer to some files, you can use any of the following: - -Arguments: -: - * `file_id`: (selective, a numerical file id) - * `file_ids`: (selective, a list of numerical file ids) - * `hash`: (selective, a hexadecimal SHA256 hash) - * `hashes`: (selective, a list of hexadecimal SHA256 hashes) - -In GET requests, make sure any list is percent-encoded. - -### **file domain** { id="parameters_file_domain" } - -When you are searching, you may want to specify a particular file domain. Most of the time, you'll want to just set `file_service_key`, but this can get complex: - -Arguments: -: - * `file_service_key`: (optional, selective A, hexadecimal, the file domain on which to search) - * `file_service_keys`: (optional, selective A, list of hexadecimals, the union of file domains on which to search) - * `deleted_file_service_key`: (optional, selective B, hexadecimal, the 'deleted from this file domain' on which to search) - * `deleted_file_service_keys`: (optional, selective B, list of hexadecimals, the union of 'deleted from this file domain' on which to search) - -The service keys are as in [/get_services](#get_services). - -Hydrus supports two concepts here: - -* Searching over a UNION of subdomains. If the user has several local file domains, e.g. 'favourites', 'personal', 'sfw', and 'nsfw', they might like to search two of them at once. -* Searching deleted files of subdomains. You can specifically, and quickly, search the files that have been deleted from somewhere. - -You can play around with this yourself by clicking 'multiple locations' in the client with _help->advanced mode_ on. - -In extreme edge cases, these two can be mixed by populating both A and B selective, making a larger union of both current and deleted file records. - -Please note that unions can be very very computationally expensive. If you can achieve what you want with a single file_service_key, two queries in a row with different service keys, or an umbrella like `all my files` or `all local files`, please do. Otherwise, let me know what is running slow and I'll have a look at it. - -'deleted from all local files' includes all files that have been physically deleted (i.e. deleted from the trash) and not available any more for fetch file/thumbnail requests. 'deleted from all my files' includes all of those physically deleted files _and_ the trash. If a file is deleted with the special 'do not leave a deletion record' command, then it won't show up in a 'deleted from file domain' search! - -'all known files' is a tricky domain. It converts much of the search tech to ignore where files actually are and look at the accompanying tag domain (e.g. all the files that have been tagged), and can sometimes be very expensive. - -Also, if you have the option to set both file and tag domains, you cannot enter 'all known files'/'all known tags'. It is too complicated to support, sorry! - -## Access Management - -### **GET `/api_version`** { id="api_version" } - -_Gets the current API version. I will increment this every time I alter the API._ - -Restricted access: NO. - -Required Headers: n/a - -Arguments: n/a - -Response: -: Some simple JSON describing the current api version (and hydrus client version, if you are interested). -: Note that this is mostly obselete now, since the 'Server' header of every response (and a duplicated 'Hydrus-Server' one, if you have a complicated proxy situation that overwrites 'Server') are now in the form "client api/{client_api_version} ({software_version})", e.g. "client api/32 (497)". - -```json title="Example response" -{ - "version" : 17, - "hydrus_version" : 441 -} -``` - -### **GET `/request_new_permissions`** { id="request_new_permissions" } - -_Register a new external program with the client. This requires the 'add from api request' mini-dialog under_ services->review services _to be open, otherwise it will 403._ - -Restricted access: NO. - -Required Headers: n/a - -Arguments: - -: * `name`: (descriptive name of your access) - * `basic_permissions`: A JSON-encoded list of numerical permission identifiers you want to request. - - The permissions are currently: - - * 0 - Import and Edit URLs - * 1 - Import and Delete Files - * 2 - Edit File Tags - * 3 - Search for and Fetch Files - * 4 - Manage Pages - * 5 - Manage Cookies - * 6 - Manage Database - * 7 - Edit File Notes - * 8 - Manage File Relationships - - ``` title="Example request" - /request_new_permissions?name=my%20import%20script&basic_permissions=[0,1] - ``` - -Response: -: Some JSON with your access key, which is 64 characters of hex. This will not be valid until the user approves the request in the client ui. -```json title="Example response" -{"access_key" : "73c9ab12751dcf3368f028d3abbe1d8e2a3a48d0de25e64f3a8f00f3a1424c57"} -``` - -### **GET `/session_key`** { id="session_key" } - -_Get a new session key._ - -Restricted access: YES. No permissions required. - -Required Headers: n/a - -Arguments: n/a - -Response: -: Some JSON with a new session key in hex. -```json title="Example response" -{ - "session_key" : "f6e651e7467255ade6f7c66050f3d595ff06d6f3d3693a3a6fb1a9c2b278f800" -} -``` - -!!! note - Note that the access you provide to get a new session key **can** be a session key, if that happens to be useful. As long as you have some kind of access, you can generate a new session key. - - A session key expires after 24 hours of inactivity, whenever the client restarts, or if the underlying access key is deleted. A request on an expired session key returns 419. - - -### **GET `/verify_access_key`** { id="verify_access_key" } - -_Check your access key is valid._ - -Restricted access: YES. No permissions required. - -Required Headers: n/a - -Arguments: n/a - -Response: -: 401/403/419 and some error text if the provided access/session key is invalid, otherwise some JSON with basic permission info. -```json title="Example response" -{ - "basic_permissions" : [0, 1, 3], - "human_description" : "API Permissions (autotagger): add tags to files, import files, search for files: Can search: only autotag this" -} -``` - - -### **GET `/get_services`** { id="get_services" } - -_Ask the client about its file and tag services._ - -Restricted access: -: YES. At least one of Add Files, Add Tags, Manage Pages, or Search Files permission needed. - -Required Headers: n/a - -Arguments: n/a - -Response: -: Some JSON listing the client's file and tag services by name and 'service key'. -```json title="Example response" -{ - "local_tags" : [ - { - "name" : "my tags", - "service_key" : "6c6f63616c2074616773", - "type" : 5, - "type_pretty" : "local tag service" - }, - { - "name" : "filenames", - "service_key" : "231a2e992b67101318c410abb6e7d98b6e32050623f138ca93bd4ad2993de31b", - "type" : 5, - "type_pretty" : "local tag service" - } - ], - "tag_repositories" : [ - { - "name" : "PTR", - "service_key" : "ccb0cf2f9e92c2eb5bd40986f72a339ef9497014a5fb8ce4cea6d6c9837877d9", - "type" : 0, - "type_pretty" : "hydrus tag repository" - } - ], - "file_repositories" : [ - { - "name" : "kamehameha central", - "service_key" : "89295dc26dae3ea7d395a1746a8fe2cb836b9472b97db48024bd05587f32ab0b", - "type" : 1, - "type_pretty" : "hydrus file repository" - } - ], - "local_files" : [ - { - "name" : "my files", - "service_key" : "6c6f63616c2066696c6573", - "type" : 2, - "type_pretty" : "local file domain" - } - ], - "all_local_media" : [ - { - "name" : "all my files", - "service_key" : "616c6c206c6f63616c206d65646961", - "type" : 21, - "type_pretty" : "virtual combined local media service" - } - ], - "trash" : [ - { - "name" : "trash", - "service_key" : "7472617368", - "type" : 14, - "type_pretty" : "local trash file domain" - } - ], - "local_updates" : [ - { - "name" : "repository updates", - "service_key" : "7265706f7369746f72792075706461746573", - "type" : 20, - "type_pretty" : "local update file domain" - } - ], - "all_local_files" : [ - { - "name" : "all local files", - "service_key" : "616c6c206c6f63616c2066696c6573", - "type" : 15, - "type_pretty" : "virtual combined local file service" - } - ], - "all_known_files" : [ - { - "name" : "all known files", - "service_key" : "616c6c206b6e6f776e2066696c6573", - "type" : 11, - "type_pretty" : "virtual combined file service" - } - ], - "all_known_tags" : [ - { - "name" : "all known tags", - "service_key" : "616c6c206b6e6f776e2074616773", - "type" : 10, - "type_pretty" : "virtual combined tag service" - } - ] -} -``` - These services may be referred to in various metadata responses or required in request parameters (e.g. where to add tag mappings). Note that a user can rename their services. The older parts of the Client API use the renameable 'service name' as service identifier, but wish to move away from this. Please use the hex 'service_key', which is a non-mutable ID specific to each client. The hardcoded services have shorter service key strings (it is usually just 'all known files' etc.. ASCII-converted to hex), but user-made stuff will have 64-character hex. - - Now that I state `type` and `type_pretty` here, I may rearrange this call, probably to make the `service_key` the Object key, rather than the arbitrary 'all_known_tags' strings. - - You won't see all these, and you'll only ever need some, but `type` is: - - * 0 - tag repository - * 1 - file repository - * 2 - a local file domain like 'my files' - * 5 - a local tag domain like 'my tags' - * 6 - a 'numerical' rating service with several stars - * 7 - a 'like/dislike' rating service with on/off status - * 10 - all known tags -- a union of all the tag services - * 11 - all known files -- a union of all the file services and files that appear in tag services - * 12 - the local booru -- you can ignore this - * 13 - IPFS - * 14 - trash - * 15 - all local files -- all files on hard disk ('all my files' + updates + trash) - * 17 - file notes - * 18 - Client API - * 19 - all deleted files -- you can ignore this - * 20 - local updates -- a file domain to store repository update files in - * 21 - all my files -- union of all local file domains - * 99 - server administration - - -## Importing and Deleting Files - -### **POST `/add_files/add_file`** { id="add_files_add_file" } - -_Tell the client to import a file._ - -Restricted access: -: YES. Import Files permission needed. - -Required Headers: -: - Content-Type: `application/json` (if sending path), `application/octet-stream` (if sending file) - -Arguments (in JSON): -: - `path`: (the path you want to import) - -```json title="Example request body" -{"path" : "E:\\to_import\\ayanami.jpg"} -``` - -Arguments (as bytes): -: You can alternately just send the file's bytes as the POST body. - -Response: -: Some JSON with the import result. Please note that file imports for large files may take several seconds, and longer if the client is busy doing other db work, so make sure your request is willing to wait that long for the response. -```json title="Example response" -{ - "status" : 1, - "hash" : "29a15ad0c035c0a0e86e2591660207db64b10777ced76565a695102a481c3dd1", - "note" : "" -} -``` - - `status` is: - - * 1 - File was successfully imported - * 2 - File already in database - * 3 - File previously deleted - * 4 - File failed to import - * 7 - File vetoed - - A file 'veto' is caused by the file import options (which in this case is the 'quiet' set under the client's _options->importing_) stopping the file due to its resolution or minimum file size rules, etc... - - 'hash' is the file's SHA256 hash in hexadecimal, and 'note' is some occasional additional human-readable text appropriate to the file status that you may recognise from hydrus's normal import workflow. For an import error, it will always be the full traceback. - - -### **POST `/add_files/delete_files`** { id="add_files_delete_files" } - -_Tell the client to send files to the trash._ - -Restricted access: -: YES. Import Files permission needed. - -Required Headers: -: -* `Content-Type`: `application/json` - -Arguments (in JSON): -: -* [files](#parameters_files) -* [file domain](#parameters_file_domain) (optional, defaults to 'all my files') -* `reason`: (optional, string, the reason attached to the delete action) - -```json title="Example request body" -{"hash" : "78f92ba4a786225ee2a1236efa6b7dc81dd729faf4af99f96f3e20bad6d8b538"} -``` - -Response: -: 200 and no content. - -If you specify a file service, the file will only be deleted from that location. Only local file domains are allowed (so you can't delete from a file repository or unpin from ipfs yet). It defaults to 'all my files', which will delete from all local services (i.e. force sending to trash). Sending 'all local files' on a file already in the trash will trigger a physical file delete. - -### **POST `/add_files/undelete_files`** { id="add_files_undelete_files" } - -_Tell the client to pull files back out of the trash._ - -Restricted access: -: YES. Import Files permission needed. - -Required Headers: -: -* `Content-Type`: application/json - -Arguments (in JSON): -: -* [files](#parameters_files) -* [file domain](#parameters_file_domain) (optional, defaults to 'all my files') - -```json title="Example request body" -{"hash" : "78f92ba4a786225ee2a1236efa6b7dc81dd729faf4af99f96f3e20bad6d8b538"} -``` - -Response: -: 200 and no content. - -You can use hash or hashes, whichever is more convenient. - -This is the reverse of a delete_files--removing files from trash and putting them back where they came from. If you specify a file service, the files will only be undeleted to there (if they have a delete record, otherwise this is nullipotent). The default, 'all my files', undeletes to all local file services for which there are deletion records. There is no error if any of the files do not currently exist in 'trash'. - - -### **POST `/add_files/archive_files`** { id="add_files_archive_files" } - -_Tell the client to archive inboxed files._ - -Restricted access: -: YES. Import Files permission needed. - -Required Headers: -: -* `Content-Type`: application/json - -Arguments (in JSON): -: -* [files](#parameters_files) - -```json title="Example request body" -{"hash" : "78f92ba4a786225ee2a1236efa6b7dc81dd729faf4af99f96f3e20bad6d8b538"} -``` - -Response: -: 200 and no content. - -This puts files in the 'archive', taking them out of the inbox. It only has meaning for files currently in 'my files' or 'trash'. There is no error if any files do not currently exist or are already in the archive. - - -### **POST `/add_files/unarchive_files`** { id="add_files_unarchive_files" } - -_Tell the client re-inbox archived files._ - -Restricted access: -: YES. Import Files permission needed. - -Required Headers: -: -* `Content-Type`: application/json - -Arguments (in JSON): -: -* [files](#parameters_files) - -```json title="Example request body" -{"hash" : "78f92ba4a786225ee2a1236efa6b7dc81dd729faf4af99f96f3e20bad6d8b538"} -``` - -Response: -: 200 and no content. - -This puts files back in the inbox, taking them out of the archive. It only has meaning for files currently in 'my files' or 'trash'. There is no error if any files do not currently exist or are already in the inbox. - - -## Importing and Editing URLs - -### **GET `/add_urls/get_url_files`** { id="add_urls_get_url_files" } - -_Ask the client about an URL's files._ - -Restricted access: -: YES. Import URLs permission needed. - -Required Headers: n/a - -Arguments: -: - * `url`: (the url you want to ask about) - * `doublecheck_file_system`: true or false (optional, defaults False) - -Example request: -: for URL `http://safebooru.org/index.php?page=post&s=view&id=2753608`: - ``` - /add_urls/get_url_files?url=http%3A%2F%2Fsafebooru.org%2Findex.php%3Fpage%3Dpost%26s%3Dview%26id%3D2753608 - ``` - -Response: -: Some JSON which files are known to be mapped to that URL. Note this needs a database hit, so it may be delayed if the client is otherwise busy. Don't rely on this to always be fast. -```json title="Example response" -{ - "normalised_url" : "https://safebooru.org/index.php?id=2753608&page=post&s=view", - "url_file_statuses" : [ - { - "status" : 2, - "hash" : "20e9002824e5e7ffc240b91b6e4a6af552b3143993c1778fd523c30d9fdde02c", - "note" : "url recognised: Imported at 2015/10/18 10:58:01, which was 3 years 4 months ago (before this check)." - } - ] -} -``` - -The `url_file_statuses` is a list of zero-to-n JSON Objects, each representing a file match the client found in its database for the URL. Typically, it will be of length 0 (for as-yet-unvisited URLs or Gallery/Watchable URLs that are not attached to files) or 1, but sometimes multiple files are given the same URL (sometimes by mistaken misattribution, sometimes by design, such as pixiv manga pages). Handling n files per URL is a pain but an unavoidable issue you should account for. - -`status` is the same as for `/add_files/add_file`: - - * 0 - File not in database, ready for import (you will only see this very rarely--usually in this case you will just get no matches) - * 2 - File already in database - * 3 - File previously deleted - -`hash` is the file's SHA256 hash in hexadecimal, and 'note' is some occasional additional human-readable text you may recognise from hydrus's normal import workflow. - -If you set `doublecheck_file_system` to `true`, then any result that is 'already in db' (2) will be double-checked against the actual file system. This check happens on any normal file import process, just to check for and fix missing files (if the file is missing, the status becomes 0--new), but the check can take more than a few milliseconds on an HDD or a network drive, so the default behaviour, assuming you mostly just want to spam for 'seen this before' file statuses, is to not do it. - -### **GET `/add_urls/get_url_info`** { id="add_urls_get_url_info" } - -_Ask the client for information about a URL._ - -Restricted access: -: YES. Import URLs permission needed. - -Required Headers: n/a - -Arguments: -: - * `url`: (the url you want to ask about) - -Example request: -: for URL `https://8ch.net/tv/res/1846574.html`: - ``` - /add_urls/get_url_info?url=https%3A%2F%2F8ch.net%2Ftv%2Fres%2F1846574.html - ``` - -Response: -: Some JSON describing what the client thinks of the URL. -```json title="Example response" -{ - "normalised_url" : "https://8ch.net/tv/res/1846574.html", - "url_type" : 4, - "url_type_string" : "watchable url", - "match_name" : "8chan thread", - "can_parse" : true -} -``` - - The url types are currently: - - * 0 - Post URL - * 2 - File URL - * 3 - Gallery URL - * 4 - Watchable URL - * 5 - Unknown URL (i.e. no matching URL Class) - - 'Unknown' URLs are treated in the client as direct File URLs. Even though the 'File URL' type is available, most file urls do not have a URL Class, so they will appear as Unknown. Adding them to the client will pass them to the URL Downloader as a raw file for download and import. - - -### **POST `/add_urls/add_url`** { id="add_urls_add_url" } - -_Tell the client to 'import' a URL. This triggers the exact same routine as drag-and-dropping a text URL onto the main client window._ - -Restricted access: -: YES. Import URLs permission needed. Add Tags needed to include tags. - -Required Headers: -: - * `Content-Type`: `application/json` - -Arguments (in JSON): -: - - * `url`: (the url you want to add) - * `destination_page_key`: (optional page identifier for the page to receive the url) - * `destination_page_name`: (optional page name to receive the url) - * `show_destination_page`: (optional, defaulting to false, controls whether the UI will change pages on add) - * `service_keys_to_additional_tags`: (optional, selective, tags to give to any files imported from this url) - * `filterable_tags`: (optional tags to be filtered by any tag import options that applies to the URL) - -If you specify a `destination_page_name` and an appropriate importer page already exists with that name, that page will be used. Otherwise, a new page with that name will be recreated (and used by subsequent calls with that name). Make sure it that page name is unique (e.g. '/b/ threads', not 'watcher') in your client, or it may not be found. - -Alternately, `destination_page_key` defines exactly which page should be used. Bear in mind this page key is only valid to the current session (they are regenerated on client reset or session reload), so you must figure out which one you want using the [/manage\_pages/get\_pages](#manage_pages_get_pages) call. If the correct page_key is not found, or the page it corresponds to is of the incorrect type, the standard page selection/creation rules will apply. - -`show_destination_page` defaults to False to reduce flicker when adding many URLs to different pages quickly. If you turn it on, the client will behave like a URL drag and drop and select the final page the URL ends up on. - -`service_keys_to_additional_tags` uses the same data structure as in /add\_tags/add\_tags--service keys to a list of tags to add. You will need 'add tags' permission or this will 403. These tags work exactly as 'additional' tags work in a _tag import options_. They are service specific, and always added unless some advanced tag import options checkbox (like 'only add tags to new files') is set. - -filterable_tags works like the tags parsed by a hydrus downloader. It is just a list of strings. They have no inherant service and will be sent to a _tag import options_, if one exists, to decide which tag services get what. This parameter is useful if you are pulling all a URL's tags outside of hydrus and want to have them processed like any other downloader, rather than figuring out service names and namespace filtering on your end. Note that in order for a tag import options to kick in, I think you will have to have a Post URL URL Class hydrus-side set up for the URL so some tag import options (whether that is Class-specific or just the default) can be loaded at import time. - -```json title="Example request body" -{ - "url" : "https://8ch.net/tv/res/1846574.html", - "destination_page_name" : "kino zone", - "service_keys_to_additional_tags" : { - "6c6f63616c2074616773" : ["as seen on /tv/"] - } -} -``` -```json title="Example request body" -{ - "url" : "https://safebooru.org/index.php?page=post&s=view&id=3195917", - "filterable_tags" : [ - "1girl", - "artist name", - "creator:azto dio", - "blonde hair", - "blue eyes", - "breasts", - "character name", - "commentary", - "english commentary", - "formal", - "full body", - "glasses", - "gloves", - "hair between eyes", - "high heels", - "highres", - "large breasts", - "long hair", - "long sleeves", - "looking at viewer", - "series:metroid", - "mole", - "mole under mouth", - "patreon username", - "ponytail", - "character:samus aran", - "solo", - "standing", - "suit", - "watermark" - ] -} -``` - -Response: -: Some JSON with info on the URL added. -```json title="Example response" -{ - "human_result_text" : "\"https://8ch.net/tv/res/1846574.html\" URL added successfully.", - "normalised_url" : "https://8ch.net/tv/res/1846574.html" -} -``` - - - -### **POST `/add_urls/associate_url`** { id="add_urls_associate_url" } - -_Manage which URLs the client considers to be associated with which files._ - -Restricted access: -: YES. Import URLs permission needed. - -Required Headers: -: - * `Content-Type`: `application/json` - - -Arguments (in JSON): -: - * `url_to_add`: (optional, selective A, an url you want to associate with the file(s)) - * `urls_to_add`: (optional, selective A, a list of urls you want to associate with the file(s)) - * `url_to_delete`: (optional, selective B, an url you want to disassociate from the file(s)) - * `urls_to_delete`: (optional, selective B, a list of urls you want to disassociate from the file(s)) - * [files](#parameters_files) - - The single/multiple arguments work the same--just use whatever is convenient for you. Unless you really know what you are doing with URL Classes, I strongly recommend you stick to associating URLs with just one single 'hash' at a time. Multiple hashes pointing to the same URL is unusual and frequently unhelpful. -```json title="Example request body" -{ - "url_to_add" : "https://rule34.xxx/index.php?id=2588418&page=post&s=view", - "hash" : "3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba" -} -``` - -Response: -: 200 with no content. Like when adding tags, this is safely idempotent--do not worry about re-adding URLs associations that already exist or accidentally trying to delete ones that don't. - - -## Editing File Tags - -### **GET `/add_tags/clean_tags`** { id="add_tags_clean_tags" } - -_Ask the client about how it will see certain tags._ - -Restricted access: -: YES. Add Tags permission needed. - -Required Headers: n/a - -Arguments (in percent-encoded JSON): -: -* `tags`: (a list of the tags you want cleaned) - -Example request: -: Given tags `#!json [ " bikini ", "blue eyes", " character : samus aran ", " :)", " ", "", "10", "11", "9", "system:wew", "-flower" ]`: - ``` - /add_tags/clean_tags?tags=%5B%22%20bikini%20%22%2C%20%22blue%20%20%20%20eyes%22%2C%20%22%20character%20%3A%20samus%20aran%20%22%2C%20%22%3A%29%22%2C%20%22%20%20%20%22%2C%20%22%22%2C%20%2210%22%2C%20%2211%22%2C%20%229%22%2C%20%22system%3Awew%22%2C%20%22-flower%22%5D - ``` - -Response: -: The tags cleaned according to hydrus rules. They will also be in hydrus human-friendly sorting order. -```json title="Example response" -{ - "tags" : ["9", "10", "11", " ::)", "bikini", "blue eyes", "character:samus aran", "flower", "wew"] -} -``` - - Mostly, hydrus simply trims excess whitespace, but the other examples are rare issues you might run into. 'system' is an invalid namespace, tags cannot be prefixed with hyphens, and any tag starting with ':' is secretly dealt with internally as "\[no namespace\]:\[colon-prefixed-subtag\]". Again, you probably won't run into these, but if you see a mismatch somewhere and want to figure it out, or just want to sort some numbered tags, you might like to try this. - - -### **GET `/add_tags/search_tags`** { id="add_tags_search_tags" } - -_Search the client for tags._ - -Restricted access: -: YES. Search for Files permission needed. - -Required Headers: n/a - -Arguments: -: - * `search`: (the tag text to search for, enter exactly what you would in the client UI) - * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') - * `tag_service_key`: (optional, hexadecimal, the tag domain on which to search, defaults to 'all known tags') - * `tag_display_type`: (optional, string, to select whether to search raw or sibling-processed tags, defaults to 'storage') - -The `file domain` and `tag_service_key` perform the function of the file and tag domain buttons in the client UI. - -The `tag_display_type` can be either `storage` (the default), which searches your file's stored tags, just as they appear in a 'manage tags' dialog, or `display`, which searches the sibling-processed tags, just as they appear in a normal file search page. In the example above, setting the `tag_display_type` to `display` could well combine the two kim possible tags and give a count of 3 or 4. - -'all my files'/'all known tags' works fine for most cases, but a specific tag service or 'all known files'/'tag service' can work better for editing tag repository `storage` contexts, since it provides results just for that service, and for repositories, it gives tags for all the non-local files other users have tagged. - -Example request: -: -```http title="Example request" -/add_tags/search_tags?search=kim&tag_display_type=display -``` - -Response: -: Some JSON listing the client's matching tags. - -: -```json title="Example response" -{ - "tags" : [ - { - "value" : "series:kim possible", - "count" : 3 - }, - { - "value" : "kimchee", - "count" : 2 - }, - { - "value" : "character:kimberly ann possible", - "count" : 1 - } - ] -} -``` - -The `tags` list will be sorted by descending count. The various rules in _tags->manage tag display and search_ (e.g. no pure `*` searches on certain services) will also be checked--and if violated, you will get 200 OK but an empty result. - -Note that if your client api access is only allowed to search certain tags, the results will be similarly filtered. - -### **POST `/add_tags/add_tags`** { id="add_tags_add_tags" } - -_Make changes to the tags that files have._ - -Restricted access: -: YES. Add Tags permission needed. - -Required Headers: n/a - -Arguments (in JSON): -: -* [files](#parameters_files) -* `service_keys_to_tags`: (selective B, an Object of service keys to lists of tags to be 'added' to the files) -* `service_keys_to_actions_to_tags`: (selective B, an Object of service keys to content update actions to lists of tags) - - In 'service\_keys\_to...', the keys are as in [/get_services](#get_services). You may need some selection UI on your end so the user can pick what to do if there are multiple choices. - - Also, you can use either '...to\_tags', which is simple and add-only, or '...to\_actions\_to\_tags', which is more complicated and allows you to remove/petition or rescind pending content. - - The permitted 'actions' are: - - * 0 - Add to a local tag service. - * 1 - Delete from a local tag service. - * 2 - Pend to a tag repository. - * 3 - Rescind a pend from a tag repository. - * 4 - Petition from a tag repository. (This is special) - * 5 - Rescind a petition from a tag repository. - - When you petition a tag from a repository, a 'reason' for the petition is typically needed. If you send a normal list of tags here, a default reason of "Petitioned from API" will be given. If you want to set your own reason, you can instead give a list of \[ tag, reason \] pairs. - -Some example requests: -: -```json title="Adding some tags to a file" -{ - "hash" : "df2a7b286d21329fc496e3aa8b8a08b67bb1747ca32749acb3f5d544cbfc0f56", - "service_keys_to_tags" : { - "6c6f63616c2074616773" : ["character:supergirl", "rating:safe"] - } -} -``` -```json title="Adding more tags to two files" -{ - "hashes" : [ - "df2a7b286d21329fc496e3aa8b8a08b67bb1747ca32749acb3f5d544cbfc0f56", - "f2b022214e711e9a11e2fcec71bfd524f10f0be40c250737a7861a5ddd3faebf" - ], - "service_keys_to_tags" : { - "6c6f63616c2074616773" : ["process this"], - "ccb0cf2f9e92c2eb5bd40986f72a339ef9497014a5fb8ce4cea6d6c9837877d9" : ["creator:dandon fuga"] - } -} -``` -```json title="A complicated transaction with all possible actions" -{ - "hash" : "df2a7b286d21329fc496e3aa8b8a08b67bb1747ca32749acb3f5d544cbfc0f56", - "service_keys_to_actions_to_tags" : { - "6c6f63616c2074616773" : { - "0" : ["character:supergirl", "rating:safe"], - "1" : ["character:superman"] - }, - "aa0424b501237041dab0308c02c35454d377eebd74cfbc5b9d7b3e16cc2193e9" : { - "2" : ["character:supergirl", "rating:safe"], - "3" : ["filename:image.jpg"], - "4" : [["creator:danban faga", "typo"], ["character:super_girl", "underscore"]], - "5" : ["skirt"] - } - } -} -``` - - This last example is far more complicated than you will usually see. Pend rescinds and petition rescinds are not common. Petitions are also quite rare, and gathering a good petition reason for each tag is often a pain. - - Note that the enumerated status keys in the service\_keys\_to\_actions\_to_tags structure are strings, not ints (JSON does not support int keys for Objects). - -Response description: -: 200 and no content. - -!!! note - Note also that hydrus tag actions are safely idempotent. You can pend a tag that is already pended, or add a tag that already exists, and not worry about an error--the surplus add action will be discarded. The same is true if you try to pend a tag that actually already exists, or rescinding a petition that doesn't. Any invalid actions will fail silently. - - It is fine to just throw your 'process this' tags at every file import and not have to worry about checking which files you already added them to. - -!!! danger "HOWEVER" - When you delete a tag, a deletion record is made _even if the tag does not exist on the file_. This is important if you expect to add the tags again via parsing, because, in general, when hydrus adds tags through a downloader, it will not overwrite a previously 'deleted' tag record (this is to stop re-downloads overwriting the tags you hand-removed previously). Undeletes usually have to be done manually by a human. - - So, _do_ be careful about how you spam delete unless it is something that doesn't matter or it is something you'll only be touching again via the API anyway. - -## Editing File Notes - -### **POST `/add_notes/set_notes`** { id="add_notes_set_notes" } - -_Add or update notes associated with a file._ - -Restricted access: -: YES. Add Notes permission needed. - -Required Headers: -: - * `Content-Type`: `application/json` - -Arguments (in percent-encoded JSON): -: -* `notes`: (an Object mapping string names to string texts) -* `hash`: (selective, an SHA256 hash for the file in 64 characters of hexadecimal) -* `file_id`: (selective, the integer numerical identifier for the file) -* `merge_cleverly`: true or false (optional, defaults false) -* `extend_existing_note_if_possible`: true or false (optional, defaults true) -* `conflict_resolution`: 0, 1, 2, or 3 (optional, defaults 3) - -With `merge_cleverly` left `false`, then this is a simple update operation. Existing notes will be overwritten exactly as you specify. Any other notes the file has will be untouched. -```json title="Example request body" -{ - "notes" : { - "note name" : "content of note", - "another note" : "asdf" - }, - "hash" : "3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba" -} -``` - -If you turn on `merge_cleverly`, then the client will merge your new notes into the file's existing notes using the same logic you have seen in Note Import Options and the Duplicate Metadata Merge Options. This navigates conflict resolution, and you should use it if you are adding potential duplicate content from an 'automatic' source like a parser and do not want to wade into the logic. Do not use it for a user-editing experience (a user expects a strict overwrite/replace experience and will be confused by this mode). - -To start off, in this mode, if your note text exists under a different name for the file, your dupe note will not be added to your new name. `extend_existing_note_if_possible` makes it so your existing note text will overwrite an existing name (or a '... (1)' rename of that name) if the existing text is inside your given text. `conflict_resolution` is an enum governing what to do in all other conflicts: - -_If a new note name already exists and its new text differs from what already exists:_ -: -* 0 - replace - Overwrite the existing conflicting note. -* 1 - ignore - Make no changes. -* 2 - append - Append the new text to the existing text. -* 3 - rename (default) - Add the new text under a 'name (x)'-style rename. - -Response: -: 200 with the note changes actually sent through. If `merge_cleverly=false`, this is exactly what you gave, and this operation is idempotent. If `merge_cleverly=true`, then this may differ, even be empty, and this operation might not be idempotent. -```json title="Example response" -{ - "notes" : { - "note name" : "content of note", - "another note (1)" : "asdf" - } -} -``` - - -### **POST `/add_notes/delete_notes`** { id="add_notes_delete_notes" } - -_Remove notes associated with a file._ - -Restricted access: -: YES. Add Notes permission needed. - -Required Headers: -: - * `Content-Type`: `application/json` - -Arguments (in percent-encoded JSON): -: -* `note_names`: (a list of string note names to delete) -* `hash`: (selective, an SHA256 hash for the file in 64 characters of hexadecimal) -* `file_id`: (selective, the integer numerical identifier for the file) - -```json title="Example request body" -{ - "note_names" : ["note name", "another note"], - "hash" : "3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba" -} -``` - -Response: -: 200 with no content. This operation is idempotent. - -## Searching and Fetching Files - -File search in hydrus is not paginated like a booru--all searches return all results in one go. In order to keep this fast, search is split into two steps--fetching file identifiers with a search, and then fetching file metadata in batches. You may have noticed that the client itself performs searches like this--thinking a bit about a search and then bundling results in batches of 256 files before eventually throwing all the thumbnails on screen. - -### **GET `/get_files/search_files`** { id="get_files_search_files" } - -_Search for the client's files._ - -Restricted access: -: YES. Search for Files permission needed. Additional search permission limits may apply. - -Required Headers: n/a - -Arguments (in percent-encoded JSON): -: - * `tags`: (a list of tags you wish to search for) - * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') - * `tag_service_key`: (optional, hexadecimal, the tag domain on which to search, defaults to 'all my files') - * `file_sort_type`: (optional, integer, the results sort method, defaults to 'all known tags') - * `file_sort_asc`: true or false (optional, the results sort order) - * `return_file_ids`: true or false (optional, default true, returns file id results) - * `return_hashes`: true or false (optional, default false, returns hex hash results) - -``` title='Example request for 16 files (system:limit=16) in the inbox with tags "blue eyes", "blonde hair", and "кино"' -/get_files/search_files?tags=%5B%22blue%20eyes%22%2C%20%22blonde%20hair%22%2C%20%22%5Cu043a%5Cu0438%5Cu043d%5Cu043e%22%2C%20%22system%3Ainbox%22%2C%20%22system%3Alimit%3D16%22%5D -``` - - -If the access key's permissions only permit search for certain tags, at least one positive whitelisted/non-blacklisted tag must be in the "tags" list or this will 403. Tags can be prepended with a hyphen to make a negated tag (e.g. "-green eyes"), but these will not be checked against the permissions whitelist. - -Wildcards and namespace searches are supported, so if you search for 'character:sam*' or 'series:*', this will be handled correctly clientside. - -**Many system predicates are also supported using a text parser!** The parser was designed by a clever user for human input and allows for a certain amount of error (e.g. ~= instead of ≈, or "isn't" instead of "is not") or requires more information (e.g. the specific hashes for a hash lookup). **Here's a big list of examples that are supported:** - -??? example "System Predicates" - * system:everything - * system:inbox - * system:archive - * system:has duration - * system:no duration - * system:is the best quality file of its duplicate group - * system:is not the best quality file of its duplicate group - * system:has audio - * system:no audio - * system:has exif - * system:no exif - * system:has human-readable embedded metadata - * system:no human-readable embedded metadata - * system:has icc profile - * system:no icc profile - * system:has tags - * system:no tags - * system:untagged - * system:number of tags > 5 - * system:number of tags ~= 10 - * system:number of tags > 0 - * system:number of words < 2 - * system:height = 600 - * system:height > 900 - * system:width < 200 - * system:width > 1000 - * system:filesize ~= 50 kilobytes - * system:filesize > 10megabytes - * system:filesize < 1 GB - * system:filesize > 0 B - * system:similar to abcdef01 abcdef02 abcdef03, abcdef04 with distance 3 - * system:similar to abcdef distance 5 - * system:limit = 100 - * system:filetype = image/jpg, image/png, apng - * system:hash = abcdef01 abcdef02 abcdef03 _(this does sha256)_ - * system:hash = abcdef01 abcdef02 md5 - * system:modified date < 7 years 45 days 7h - * system:modified date > 2011-06-04 - * system:last viewed time < 7 years 45 days 7h - * system:last view time < 7 years 45 days 7h - * system:date modified > 7 years 2 months - * system:date modified < 0 years 1 month 1 day 1 hour - * system:import time < 7 years 45 days 7h - * system:time imported < 7 years 45 days 7h - * system:time imported > 2011-06-04 - * system:time imported > 7 years 2 months - * system:time imported < 0 years 1 month 1 day 1 hour - * system:time imported ~= 2011-1-3 - * system:time imported ~= 1996-05-2 - * system:duration < 5 seconds - * system:duration ~= 600 msecs - * system:duration > 3 milliseconds - * system:file service is pending to my files - * system:file service currently in my files - * system:file service is not currently in my files - * system:file service is not pending to my files - * system:num file relationships < 3 alternates - * system:number of file relationships > 3 false positives - * system:ratio is wider than 16:9 - * system:ratio is 16:9 - * system:ratio taller than 1:1 - * system:num pixels > 50 px - * system:num pixels < 1 megapixels - * system:num pixels ~= 5 kilopixel - * system:media views ~= 10 - * system:all views > 0 - * system:preview views < 10 - * system:media viewtime < 1 days 1 hour 0 minutes - * system:all viewtime > 1 hours 100 seconds - * system:preview viewtime ~= 1 day 30 hours 100 minutes 90s - * system:has url matching regex index\\.php - * system:does not have a url matching regex index\\.php - * system:has url https://safebooru.donmai.us/posts/4695284 - * system:does not have url https://safebooru.donmai.us/posts/4695284 - * system:has domain safebooru.com - * system:does not have domain safebooru.com - * system:has a url with class safebooru file page - * system:does not have a url with url class safebooru file page - * system:tag as number page < 5 - * system:has notes - * system:no notes - * system:does not have notes - * system:num notes is 5 - * system:num notes > 1 - * system:has note with name note name - * system:no note with name note name - * system:does not have note with name note name - -Please test out the system predicates you want to send. If you are in _help->advanced mode_, you can test this parser in the advanced text input dialog when you click the OR\* button on a tag autocomplete dropdown. More system predicate types and input formats will be available in future. Reverse engineering system predicate data from text is obviously tricky. If a system predicate does not parse, you'll get 400. - -Also, OR predicates are now supported! Just nest within the tag list, and it'll be treated like an OR. For instance: - -* `#!json [ "skirt", [ "samus aran", "lara croft" ], "system:height > 1000" ]` - -Makes: - -* skirt -* samus aran OR lara croft -* system:height > 1000 - -The file and tag services are for search domain selection, just like clicking the buttons in the client. They are optional--default is 'all my files' and 'all known tags'. - -File searches occur in the `display` `tag_display_type`. If you want to pair autocomplete tag lookup from [/search_tags](#add_tags_search_tags) to this file search (e.g. for making a standard booru search interface), then make sure you are searching `display` tags there. - -file\_sort\_asc is 'true' for ascending, and 'false' for descending. The default is descending. - -file\_sort\_type is by default _import time_. It is an integer according to the following enum, and I have written the semantic (asc/desc) meaning for each type after: - -* 0 - file size (smallest first/largest first) -* 1 - duration (shortest first/longest first) -* 2 - import time (oldest first/newest first) -* 3 - filetype (N/A) -* 4 - random (N/A) -* 5 - width (slimmest first/widest first) -* 6 - height (shortest first/tallest first) -* 7 - ratio (tallest first/widest first) -* 8 - number of pixels (ascending/descending) -* 9 - number of tags (on the current tag domain) (ascending/descending) -* 10 - number of media views (ascending/descending) -* 11 - total media viewtime (ascending/descending) -* 12 - approximate bitrate (smallest first/largest first) -* 13 - has audio (audio first/silent first) -* 14 - modified time (oldest first/newest first) -* 15 - framerate (slowest first/fastest first) -* 16 - number of frames (smallest first/largest first) -* 18 - last viewed time (oldest first/newest first) -* 19 - archive timestamp (oldest first/newest first) -* 20 - hash hex (N/A) - -Response: -: The full list of numerical file ids that match the search. -```json title="Example response" -{ - "file_ids" : [125462, 4852415, 123, 591415] -} -``` -```json title="Example response with return_hashes=true" -{ - "hashes" : [ - "1b04c4df7accd5a61c5d02b36658295686b0abfebdc863110e7d7249bba3f9ad", - "fe416723c731d679aa4d20e9fd36727f4a38cd0ac6d035431f0f452fad54563f", - "b53505929c502848375fbc4dab2f40ad4ae649d34ef72802319a348f81b52bad" - ], - "file_ids" : [125462, 4852415, 123] -} -``` - - You can of course also specify `return_hashes=true&return_file_ids=false` just to get the hashes. The order of both lists is the same. - - File ids are internal and specific to an individual client. For a client, a file with hash H always has the same file id N, but two clients will have different ideas about which N goes with which H. IDs are a bit faster to retrieve than hashes and search with _en masse_, which is why they are exposed here. - - This search does **not** apply the implicit limit that most clients set to all searches (usually 10,000), so if you do system:everything on a client with millions of files, expect to get boshed. Even with a system:limit included, complicated queries with large result sets may take several seconds to respond. Just like the client itself. - -### **GET `/get_files/file_hashes`** { id="get_files_file_hashes" } - -_Lookup file hashes from other hashes._ - -Restricted access: -: YES. Search for Files permission needed. - -Required Headers: n/a - -Arguments (in percent-encoded JSON): -: - * `hash`: (selective, a hexadecimal hash) - * `hashes`: (selective, a list of hexadecimal hashes) - * `source_hash_type`: [sha256|md5|sha1|sha512] (optional, defaulting to sha256) - * `desired_hash_type`: [sha256|md5|sha1|sha512] - -If you have some MD5 hashes and want to see what their SHA256 are, or _vice versa_, this is the place. Hydrus records the non-SHA256 hashes for every file it has ever imported. This data is not removed on file deletion. - -``` title="Example request" -/get_files/file_hashes?hash=ec5c5a4d7da4be154597e283f0b6663c&source_hash_type=md5&desired_hash_type=sha256 -``` - -Response: -: A mapping Object of the successful lookups. Where no matching hash is found, no entry will be made (therefore, if none of your source hashes have matches on the client, this will return an empty `hashes` Object). -```json title="Example response" -{ - "hashes" : { - "ec5c5a4d7da4be154597e283f0b6663c" : "2a0174970defa6f147f2eabba829c5b05aba1f1aea8b978611a07b7bb9cf9399" - } -} -``` - -### **GET `/get_files/file_metadata`** { id="get_files_file_metadata" } - -_Get metadata about files in the client._ - -Restricted access: -: YES. Search for Files permission needed. Additional search permission limits may apply. - -Required Headers: n/a - -Arguments (in percent-encoded JSON): -: - * [files](#parameters_files) - * `create_new_file_ids`: true or false (optional if asking with hash(es), defaulting to false) - * `only_return_identifiers`: true or false (optional, defaulting to false) - * `only_return_basic_information`: true or false (optional, defaulting to false) - * `detailed_url_information`: true or false (optional, defaulting to false) - * `include_notes`: true or false (optional, defaulting to false) - * `hide_service_keys_tags`: **Deprecated, will be deleted soon!** true or false (optional, defaulting to true) - -You need one of file_ids or hashes. If your access key is restricted by tag, you cannot search by hashes, and **the file_ids you search for must have been in the most recent search result**. - -``` title="Example request for two files with ids 123 and 4567" -/get_files/file_metadata?file_ids=%5B123%2C%204567%5D -``` - -``` title="The same, but only wants hashes back" -/get_files/file_metadata?file_ids=%5B123%2C%204567%5D&only_return_identifiers=true -``` - -``` title="And one that fetches two hashes" -/get_files/file_metadata?hashes=%5B%224c77267f93415de0bc33b7725b8c331a809a924084bee03ab2f5fae1c6019eb2%22%2C%20%223e7cb9044fe81bda0d7a84b5cb781cba4e255e4871cba6ae8ecd8207850d5b82%22%5D -``` - -This request string can obviously get pretty ridiculously long. It also takes a bit of time to fetch metadata from the database. In its normal searches, the client usually fetches file metadata in batches of 256. - -Response: -: A list of JSON Objects that store a variety of file metadata. -```json title="Example response" -{ - "metadata" : [ - { - "file_id" : 123, - "hash" : "4c77267f93415de0bc33b7725b8c331a809a924084bee03ab2f5fae1c6019eb2", - "size" : 63405, - "mime" : "image/jpeg", - "ext" : ".jpg", - "width" : 640, - "height" : 480, - "thumbnail_width" : 200, - "thumbnail_height" : 150, - "duration" : null, - "time_modified" : null, - "time_modified_details" : {}, - "file_services" : { - "current" : {}, - "deleted" : {} - }, - "ipfs_multihashes" : {}, - "has_audio" : false, - "num_frames" : null, - "num_words" : null, - "is_inbox" : false, - "is_local" : false, - "is_trashed" : false, - "is_deleted" : false, - "has_exif" : true, - "has_human_readable_embedded_metadata" : true, - "has_icc_profile" : true, - "known_urls" : [], - "tags" : { - "6c6f63616c2074616773" : { - "name" : "local tags", - "type" : 5, - "type_pretty" : "local tag service", - "storage_tags" : {}, - "display_tags" : {} - }, - "37e3849bda234f53b0e9792a036d14d4f3a9a136d1cb939705dbcd5287941db4" : { - "name" : "public tag repo", - "type" : 1, - "type_pretty" : "hydrus tag repository", - "storage_tags" : {}, - "display_tags" : {} - }, - "616c6c206b6e6f776e2074616773" : { - "name" : "all known tags", - "type" : 10, - "type_pretty" : "virtual combined tag service", - "storage_tags" : {}, - "display_tags" : {} - } - } - }, - { - "file_id" : 4567, - "hash" : "3e7cb9044fe81bda0d7a84b5cb781cba4e255e4871cba6ae8ecd8207850d5b82", - "size" : 199713, - "mime" : "video/webm", - "ext" : ".webm", - "width" : 1920, - "height" : 1080, - "thumbnail_width" : 200, - "thumbnail_height" : 113, - "duration" : 4040, - "time_modified" : 1604055647, - "time_modified_details" : { - "local" : 1641044491, - "gelbooru.com" : 1604055647 - }, - "file_services" : { - "current" : { - "616c6c206c6f63616c2066696c6573" : { - "name" : "all local files", - "type" : 15, - "type_pretty" : "virtual combined local file service", - "time_imported" : 1641044491 - }, - "616c6c206c6f63616c2066696c6573" : { - "name" : "all my files", - "type" : 21, - "type_pretty" : "virtual combined local media service", - "time_imported" : 1641044491 - }, - "cb072cffbd0340b67aec39e1953c074e7430c2ac831f8e78fb5dfbda6ec8dcbd" : { - "name" : "cool space babes", - "type" : 2, - "type_pretty" : "local file domain", - "time_imported" : 1641204220 - } - }, - "deleted" : { - "6c6f63616c2066696c6573" : { - "name" : "my files", - "type" : 2, - "type_pretty" : "local file domain", - "time_deleted" : 1641204274, - "time_imported" : 1641044491 - } - } - }, - "ipfs_multihashes" : { - "55af93e0deabd08ce15ffb2b164b06d1254daab5a18d145e56fa98f71ddb6f11" : "QmReHtaET3dsgh7ho5NVyHb5U13UgJoGipSWbZsnuuM8tb" - }, - "has_audio" : true, - "num_frames" : 102, - "num_words" : null, - "is_inbox" : false, - "is_local" : true, - "is_trashed" : false, - "is_deleted" : false, - "has_exif" : false, - "has_human_readable_embedded_metadata" : false, - "has_icc_profile" : false, - "known_urls" : [ - "https://gelbooru.com/index.php?page=post&s=view&id=4841557", - "https://img2.gelbooru.com//images/80/c8/80c8646b4a49395fb36c805f316c49a9.jpg", - "http://origin-orig.deviantart.net/ed31/f/2019/210/7/8/beachqueen_samus_by_dandonfuga-ddcu1xg.jpg" - ], - "tags" : { - "6c6f63616c2074616773" : { - "name" : "local tags", - "type" : 5, - "type_pretty" : "local tag service", - "storage_tags" : { - "0" : ["samus favourites"], - "2" : ["process this later"] - }, - "display_tags" : { - "0" : ["samus favourites", "favourites"], - "2" : ["process this later"] - } - }, - "37e3849bda234f53b0e9792a036d14d4f3a9a136d1cb939705dbcd5287941db4" : { - "name" : "public tag repo", - "type" : 1, - "type_pretty" : "hydrus tag repository", - "storage_tags" : { - "0" : ["blonde_hair", "blue_eyes", "looking_at_viewer"], - "1" : ["bodysuit"] - }, - "display_tags" : { - "0" : ["blonde hair", "blue_eyes", "looking at viewer"], - "1" : ["bodysuit", "clothing"] - } - }, - "616c6c206b6e6f776e2074616773" : { - "name" : "all known tags", - "type" : 10, - "type_pretty" : "virtual combined tag service", - "storage_tags" : { - "0" : ["samus favourites", "blonde_hair", "blue_eyes", "looking_at_viewer"], - "1" : ["bodysuit"] - }, - "display_tags" : { - "0" : ["samus favourites", "favourites", "blonde hair", "blue_eyes", "looking at viewer"], - "1" : ["bodysuit", "clothing"] - } - } - } - } - ] -} -``` -```json title="And one where only_return_identifiers is true" -{ - "metadata" : [ - { - "file_id" : 123, - "hash" : "4c77267f93415de0bc33b7725b8c331a809a924084bee03ab2f5fae1c6019eb2" - }, - { - "file_id" : 4567, - "hash" : "3e7cb9044fe81bda0d7a84b5cb781cba4e255e4871cba6ae8ecd8207850d5b82" - } - ] -} -``` -```json title="And where only_return_basic_information is true" -{ - "metadata" : [ - { - "file_id" : 123, - "hash" : "4c77267f93415de0bc33b7725b8c331a809a924084bee03ab2f5fae1c6019eb2", - "size" : 63405, - "mime" : "image/jpeg", - "ext" : ".jpg", - "width" : 640, - "height" : 480, - "duration" : null, - "has_audio" : false, - "num_frames" : null, - "num_words" : null, - }, - { - "file_id" : 4567, - "hash" : "3e7cb9044fe81bda0d7a84b5cb781cba4e255e4871cba6ae8ecd8207850d5b82", - "size" : 199713, - "mime" : "video/webm", - "ext" : ".webm", - "width" : 1920, - "height" : 1080, - "duration" : 4040, - "has_audio" : true, - "num_frames" : 102, - "num_words" : null, - } - ] -} -``` - -#### basics - -Size is in bytes. Duration is in milliseconds, and may be an int or a float. - -`is_trashed` means if the file is currently in the trash but available on the hard disk. `is_deleted` means currently either in the trash or completely deleted from disk. - -`file_services` stores which file services the file is currently in and _deleted_ from. The entries are by the service key, same as for tags later on. In rare cases, the timestamps may be `null`, if they are unknown (e.g. a `time_deleted` for the file deleted before this information was tracked). The `time_modified` can also be null. Time modified is just the filesystem modified time for now, but it will evolve into more complicated storage in future with multiple locations (website post times) that'll be aggregated to a sensible value in UI. - -`ipfs_multihashes` stores the ipfs service key to any known multihash for the file. - -The `thumbnail_width` and `thumbnail_height` are a generally reliable prediction but aren't a promise. The actual thumbnail you get from [/get_files/thumbnail](#get_files_thumbnail) will be different if the user hasn't looked at it since changing their thumbnail options. You only get these rows for files that hydrus actually generates an actual thumbnail for. Things like pdf won't have it. You can use your own thumb, or ask the api and it'll give you a fixed fallback; those are mostly 200x200, but you can and should size them to whatever you want. - -#### tags - -The 'tags' structures are undergoing transition. Previously, this was a mess of different Objects in different domains, all `service_xxx_to_xxx_tags`, but they are being transitioned to the combined `tags` Object. - -`hide_service_keys_tags` is deprecated and will be deleted soon. When set to `false`, it shows the old `service_keys_to_statuses_to_tags` and `service_keys_to_statuses_to_display_tags` Objects. - -The `tags` structures are similar to the [/add_tags/add_tags](#add_tags_add_tags) scheme, excepting that the status numbers are: - -* 0 - current -* 1 - pending -* 2 - deleted -* 3 - petitioned - -!!! note - Since JSON Object keys must be strings, these status numbers are strings, not ints. - -To learn more about service names and keys on a client, use the [/get_services](#get_services) call. - -While the 'storage_tags' represent the actual tags stored on the database for a file, 'display_tags' reflect how tags appear in the UI, after siblings are collapsed and parents are added. If you want to edit a file's tags, refer to the storage tags. If you want to render to the user, use the display tags. The display tag calculation logic is very complicated; if the storage tags change, do not try to guess the new display tags yourself--just ask the API again. - -#### parameters - -If you ask with hashes rather than file_ids, hydrus will, by default, only return results when it has seen those hashes before. This is to stop the client making thousands of new file_id records in its database if you perform a scanning operation. If you ask about a hash the client has never encountered before--for which there is no file_id--you will get this style of result: - -```json title="Missing file_id example" -{ - "metadata" : [ - { - "file_id" : null, - "hash" : "766da61f81323629f982bc1b71b5c1f9bba3f3ed61caf99906f7f26881c3ae93" - } - ] -} -``` - -You can change this behaviour with `create_new_file_ids=true`, but bear in mind you will get a fairly 'empty' metadata result with lots of 'null' lines, so this is only useful for gathering the numerical ids for later Client API work. - -If you ask about any file_ids that do not exist, you'll get 404. - -If you set `only_return_basic_information=true`, this will be much faster for first-time requests than the full metadata result, but it will be slower for repeat requests. The full metadata object is cached after first fetch, the limited file info object is not. - -If you add `detailed_url_information=true`, a new entry, `detailed_known_urls`, will be added for each file, with a list of the same structure as /`add_urls/get_url_info`. This may be an expensive request if you are querying thousands of files at once. - -```json title="For example" -{ - "detailed_known_urls": [ - { - "normalised_url": "https://gelbooru.com/index.php?id=4841557&page=post&s=view", - "url_type": 0, - "url_type_string": "post url", - "match_name": "gelbooru file page", - "can_parse": true - }, - { - "normalised_url": "https://img2.gelbooru.com//images/80/c8/80c8646b4a49395fb36c805f316c49a9.jpg", - "url_type": 5, - "url_type_string": "unknown url", - "match_name": "unknown url", - "can_parse": false - } - ] -} -``` - - -### **GET `/get_files/file`** { id="get_files_file" } - -_Get a file._ - -Restricted access: -: YES. Search for Files permission needed. Additional search permission limits may apply. - -Required Headers: n/a - -Arguments : -: - * `file_id`: (numerical file id for the file) - * `hash`: (a hexadecimal SHA256 hash for the file) - - Only use one. As with metadata fetching, you may only use the hash argument if you have access to all files. If you are tag-restricted, you will have to use a file_id in the last search you ran. - - ``` title="Example request" - /get_files/file?file_id=452158 - ``` - ``` title="Example request" - /get_files/file?hash=7f30c113810985b69014957c93bc25e8eb4cf3355dae36d8b9d011d8b0cf623a - ``` - -Response: -: The file itself. You should get the correct mime type as the Content-Type header. - - -### **GET `/get_files/thumbnail`** { id="get_files_thumbnail" } - -_Get a file's thumbnail._ - -Restricted access: -: YES. Search for Files permission needed. Additional search permission limits may apply. - -Required Headers: n/a - -Arguments: -: - * `file_id`: (numerical file id for the file) - * `hash`: (a hexadecimal SHA256 hash for the file) - - Only use one. As with metadata fetching, you may only use the hash argument if you have access to all files. If you are tag-restricted, you will have to use a file_id in the last search you ran. - -``` title="Example request" -/get_files/thumbnail?file_id=452158 -``` -``` title="Example request" -/get_files/thumbnail?hash=7f30c113810985b69014957c93bc25e8eb4cf3355dae36d8b9d011d8b0cf623a -``` - -Response: -: The thumbnail for the file. Some hydrus thumbs are jpegs, some are pngs. It should give you the correct image/jpeg or image/png Content-Type. - - If hydrus keeps no thumbnail for the filetype, for instance with pdfs, then you will get the same default 'pdf' icon you see in the client. If the file does not exist in the client, or the thumbnail was expected but is missing from storage, you will get the fallback 'hydrus' icon, again just as you would in the client itself. This request should never give a 404. - -!!! note - If you get a 'default' filetype thumbnail like the pdf or hydrus one, you will be pulling the defaults straight from the hydrus/static folder. They will most likely be 200x200 pixels. - - - -## Managing File Relationships - -This refers to the File Relationships system, which includes 'potential duplicates', 'duplicates', and 'alternates'. - -This system is pending significant rework and expansion, so please do not get too married to some of the routines here. I am mostly just exposing my internal commands, so things are a little ugly/hacked. I expect duplicate and alternate groups to get some form of official identifier in future, which may end up being the way to refer and edit things here. - -Also, at least for now, 'Manage File Relationships' permission is not going to be bound by the search permission restrictions that normal file search does. Getting this permission allows you to search anything. I expect to add this permission filtering tech in future, particularly for file domains. - -_There is more work to do here, including adding various 'dissolve'/'undo' commands to break groups apart._ - -### **GET `/manage_file_relationships/get_file_relationships`** { id="manage_file_relationships_get_file_relationships" } - -_Get the current relationships for one or more files._ - -Restricted access: -: YES. Manage File Relationships permission needed. - -Required Headers: n/a - -Arguments (in percent-encoded JSON): -: - * [files](#parameters_files) - * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') - -``` title="Example request" -/manage_file_relationships/get_file_relationships?hash=ac940bb9026c430ea9530b4f4f6980a12d9432c2af8d9d39dfc67b05d91df11d -``` - -Response: -: A JSON Object mapping the hashes to their relationships. -``` json title="Example response" -{ - "file_relationships" : { - "ac940bb9026c430ea9530b4f4f6980a12d9432c2af8d9d39dfc67b05d91df11d" : { - "is_king" : false, - "king" : "8784afbfd8b59de3dcf2c13dc1be9d7cb0b3d376803c8a7a8b710c7c191bb657", - "0" : [ - ], - "1" : [], - "3" : [ - "8bf267c4c021ae4fd7c4b90b0a381044539519f80d148359b0ce61ce1684fefe" - ], - "8" : [ - "8784afbfd8b59de3dcf2c13dc1be9d7cb0b3d376803c8a7a8b710c7c191bb657", - "3fa8ef54811ec8c2d1892f4f08da01e7fc17eed863acae897eb30461b051d5c3" - ] - } - } -} -``` - -`is_king` and `king` relate to which file is the set best of a group. The king is usually the best representative of a group if you need to do comparisons between groups, and the 'get some pairs to filter'-style commands usually try to select the kings of the various to-be-compared duplicate groups. - -The relationships you get are filtered by the file domain. If you set the file domain to 'all known files', you will get every relationship a file has, including all deleted files, which is often less useful than you would think. The default, 'all my files' is usually most useful. - -**It is possible for the king to not be available, in which case `king` is null.** The king can be unavailable in several duplicate search contexts, generally when it is outside of the set file domain. For the default domain, 'all my files', the king will be available unless the user has deleted it. You have to deal with the king being unavailable--in this situation, your best bet is to just use the file itself as its own representative. - -A file that has no duplicates is considered to be in a duplicate group of size 1 and thus is always its own king. - -The numbers are from a duplicate status enum, as so: - -* 0 - potential duplicates -* 1 - false positives -* 3 - alternates -* 8 - duplicates - -Note that because of JSON constraints, these are the string versions of the integers since they are Object keys. - -All the hashes given here are in 'all my files', i.e. not in the trash. A file may have duplicates that have long been deleted, but, like the null king above, they will not show here. - -### **GET `/manage_file_relationships/get_potentials_count`** { id="manage_file_relationships_get_potentials_count" } - -_Get the count of remaining potential duplicate pairs in a particular search domain. Exactly the same as the counts you see in the duplicate processing page._ - -Restricted access: -: YES. Manage File Relationships permission needed. - -Required Headers: n/a - -Arguments (in percent-encoded JSON): -: - * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') - * `tag_service_key_1`: (optional, default 'all known tags', a hex tag service key) - * `tags_1`: (optional, default system:everything, a list of tags you wish to search for) - * `tag_service_key_2`: (optional, default 'all known tags', a hex tag service key) - * `tags_2`: (optional, default system:everything, a list of tags you wish to search for) - * `potentials_search_type`: (optional, integer, default 0, regarding how the pairs should match the search(es)) - * `pixel_duplicates`: (optional, integer, default 1, regarding whether the pairs should be pixel duplicates) - * `max_hamming_distance`: (optional, integer, default 4, the max 'search distance' of the pairs) - -``` title="Example request" -/manage_file_relationships/get_potentials_count?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&search_type=1&pixel_duplicates=2&max_hamming_distance=0&max_num_pairs=50 -``` - -`tag_service_key` and `tags` work the same as [/get\_files/search\_files](#get_files_search_files). The `_2` variants are only useful if the `potentials_search_type` is 2. For now the file domain is locked to 'all my files'. - -`potentials_search_type` and `pixel_duplicates` are enums: - -* 0 - one file matches search 1 -* 1 - both files match search 1 -* 2 - one file matches search 1, the other 2 - --and- - -* 0 - must be pixel duplicates -* 1 - can be pixel duplicates -* 2 - must not be pixel duplicates - -The `max_hamming_distance` is the same 'search distance' you see in the Client UI. A higher number means more speculative 'similar files' search. If `pixel_duplicates` is set to 'must be', then `max_hamming_distance` is obviously ignored. - -Response: -: A JSON Object stating the count. -``` json title="Example response" -{ - "potential_duplicates_count" : 17 -} -``` - -If you confirm that a pair of potentials are duplicates, this may transitively collapse other potential pairs and decrease the count by more than 1. - -### **GET `/manage_file_relationships/get_potential_pairs`** { id="manage_file_relationships_get_potential_pairs" } - -_Get some potential duplicate pairs for a filtering workflow. Exactly the same as the 'duplicate filter' in the duplicate processing page._ - -Restricted access: -: YES. Manage File Relationships permission needed. - -Required Headers: n/a - -Arguments (in percent-encoded JSON): -: - * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') - * `tag_service_key_1`: (optional, default 'all known tags', a hex tag service key) - * `tags_1`: (optional, default system:everything, a list of tags you wish to search for) - * `tag_service_key_2`: (optional, default 'all known tags', a hex tag service key) - * `tags_2`: (optional, default system:everything, a list of tags you wish to search for) - * `potentials_search_type`: (optional, integer, default 0, regarding how the pairs should match the search(es)) - * `pixel_duplicates`: (optional, integer, default 1, regarding whether the pairs should be pixel duplicates) - * `max_hamming_distance`: (optional, integer, default 4, the max 'search distance' of the pairs) - * `max_num_pairs`: (optional, integer, defaults to client's option, how many pairs to get in a batch) - -``` title="Example request" -/manage_file_relationships/get_potential_pairs?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&search_type=1&pixel_duplicates=2&max_hamming_distance=0&max_num_pairs=50 -``` - -The search arguments work the same as [/manage\_file\_relationships/get\_potentials\_count](#manage_file_relationships_get_potentials_count). - -`max_num_pairs` is simple and just caps how many pairs you get. - -Response: -: A JSON Object listing a batch of hash pairs. -```json title="Example response" -{ - "potential_duplicate_pairs" : [ - [ "16470d6e73298cd75d9c7e8e2004810e047664679a660a9a3ba870b0fa3433d3", "7ed062dc76265d25abeee5425a859cfdf7ab26fd291f50b8de7ca381e04db079" ], - [ "eeea390357f259b460219d9589b4fa11e326403208097b1a1fbe63653397b210", "9215dfd39667c273ddfae2b73d90106b11abd5fd3cbadcc2afefa526bb226608" ], - [ "a1ea7d671245a3ae35932c603d4f3f85b0d0d40c5b70ffd78519e71945031788", "8e9592b2dfb436fe0a8e5fa15de26a34a6dfe4bca9d4363826fac367a9709b25" ] - ] -} -``` - -The selected pair sample and their order is strictly hardcoded for now (e.g. to guarantee that a decision will not invalidate any other pair in the batch, you shouldn't see the same file twice in a batch, nor two files in the same duplicate group). Treat it as the client filter does, where you fetch batches to process one after another. I expect to make it more flexible in future, in the client itself and here. - -You will see significantly fewer than `max_num_pairs` (and potential duplicate count) as you close to the last available pairs, and when there are none left, you will get an empty list. - -### **GET `/manage_file_relationships/get_random_potentials`** { id="manage_file_relationships_get_random_potentials" } - -_Get some random potentially duplicate file hashes. Exactly the same as the 'show some random potential dupes' button in the duplicate processing page._ - -Restricted access: -: YES. Manage File Relationships permission needed. - -Required Headers: n/a - -Arguments (in percent-encoded JSON): -: - * [file domain](#parameters_file_domain) (optional, defaults to 'all my files') - * `tag_service_key_1`: (optional, default 'all known tags', a hex tag service key) - * `tags_1`: (optional, default system:everything, a list of tags you wish to search for) - * `tag_service_key_2`: (optional, default 'all known tags', a hex tag service key) - * `tags_2`: (optional, default system:everything, a list of tags you wish to search for) - * `potentials_search_type`: (optional, integer, default 0, regarding how the files should match the search(es)) - * `pixel_duplicates`: (optional, integer, default 1, regarding whether the files should be pixel duplicates) - * `max_hamming_distance`: (optional, integer, default 4, the max 'search distance' of the files) - -``` title="Example request" -/manage_file_relationships/get_random_potentials?tag_service_key_1=c1ba23c60cda1051349647a151321d43ef5894aacdfb4b4e333d6c4259d56c5f&tags_1=%5B%22dupes_to_process%22%2C%20%22system%3Awidth%3C400%22%5D&search_type=1&pixel_duplicates=2&max_hamming_distance=0 -``` - -The arguments work the same as [/manage\_file\_relationships/get\_potentials\_count](#manage_file_relationships_get_potentials_count), with the caveat that `potentials_search_type` has special logic: - -* 0 - first file matches search 1 -* 1 - all files match search 1 -* 2 - first file matches search 1, the others 2 - -Essentially, the first hash is the 'master' to which the others are paired. The other files will include every matching file. - -Response: -: A JSON Object listing a group of hashes exactly as the client would. -```json title="Example response" -{ - "random_potential_duplicate_hashes" : [ - "16470d6e73298cd75d9c7e8e2004810e047664679a660a9a3ba870b0fa3433d3", - "7ed062dc76265d25abeee5425a859cfdf7ab26fd291f50b8de7ca381e04db079", - "9e0d6b928b726562d70e1f14a7b506ba987c6f9b7f2d2e723809bb11494c73e6", - "9e01744819b5ff2a84dda321e3f1a326f40d0e7f037408ded9f18a11ee2b2da8" - ] -} -``` - -If there are no potential duplicate groups in the search, this returns an empty list. - -### **POST `/manage_file_relationships/set_file_relationships`** { id="manage_file_relationships_set_kings" } - -Set the relationships to the specified file pairs. - -Restricted access: -: YES. Manage File Relationships permission needed. - -Required Headers: -: - * `Content-Type`: application/json - -Arguments (in JSON): -: - * `relationships`: (a list of Objects, one for each file-pair being set) - -Each Object is: - - * `hash_a`: (a hexadecimal SHA256 hash) - * `hash_b`: (a hexadecimal SHA256 hash) - * `relationship`: (integer enum for the relationship being set) - * `do_default_content_merge`: (bool) - * `delete_a`: (optional, bool, default false) - * `delete_b`: (optional, bool, default false) - -`hash_a` and `hash_b` are normal hex SHA256 hashes for your file pair. - -`relationship` is one of this enum: - -* 0 - set as potential duplicates -* 1 - set as false positives -* 2 - set as same quality -* 3 - set as alternates -* 4 - set A as better -* 7 - set B as better - -2, 4, and 7 all make the files 'duplicates' (8 under `get_file_relationships`), which, specifically, merges the two files' duplicate groups. 'same quality' has different duplicate content merge options to the better/worse choices, but it ultimately sets A>B. You obviously don't have to use 'B is better' if you prefer just to swap the hashes. Do what works for you. - -`do_default_content_merge` sets whether the user's duplicate content merge options should be loaded and applied to the files along with the relationship. Most operations in the client do this automatically, so the user may expect it to apply, but if you want to do content merge yourself, set this to false. - -`delete_a` and `delete_b` are booleans that select whether to delete A and/or B in the same operation as setting the relationship. You can also do this externally if you prefer. - -```json title="Example request body" -{ - "relationships" : [ - { - "hash_a" : "b54d09218e0d6efc964b78b070620a1fa19c7e069672b4c6313cee2c9b0623f2", - "hash_b" : "bbaa9876dab238dcf5799bfd8319ed0bab805e844f45cf0de33f40697b11a845", - "relationship" : 4, - "do_default_content_merge" : true, - "delete_b" : true - }, - { - "hash_a" : "22667427eaa221e2bd7ef405e1d2983846c863d40b2999ce8d1bf5f0c18f5fb2", - "hash_b" : "65d228adfa722f3cd0363853a191898abe8bf92d9a514c6c7f3c89cfed0bf423", - "relationship" : 4, - "do_default_content_merge" : true, - "delete_b" : true - }, - { - "hash_a" : "0480513ffec391b77ad8c4e57fe80e5b710adfa3cb6af19b02a0bd7920f2d3ec", - "hash_b" : "5fab162576617b5c3fc8caabea53ce3ab1a3c8e0a16c16ae7b4e4a21eab168a7", - "relationship" : 2, - "do_default_content_merge" : true - } - ] -} -``` - -Response: -: 200 with no content. - -If you try to add an invalid or redundant relationship, for instance setting files that are already duplicates as potential duplicates, no changes are made. - -This is the file relationships request that is probably most likely to change in future. I may implement content merge options. I may move from file pairs to group identifiers. When I expand alternates, those file groups are going to support more variables. - -### **POST `/manage_file_relationships/set_kings`** { id="manage_file_relationships_set_kings" } - -Set the specified files to be the kings of their duplicate groups. - -Restricted access: -: YES. Manage File Relationships permission needed. - -Required Headers: -: - * `Content-Type`: application/json - -Arguments (in JSON): -: - * [files](#parameters_files) - -```json title="Example request body" -{ - "file_id" : 123 -} -``` - -Response: -: 200 with no content. - -The files will be promoted to be the kings of their respective duplicate groups. If the file is already the king (also true for any file with no duplicates), this is idempotent. It also processes the files in the given order, so if you specify two files in the same group, the latter will be the king at the end of the request. - -## Managing Cookies and HTTP Headers - -This refers to the cookies held in the client's session manager, which are sent with network requests to different domains. - -### **GET `/manage_cookies/get_cookies`** { id="manage_cookies_get_cookies" } - -_Get the cookies for a particular domain._ - -Restricted access: -: YES. Manage Cookies permission needed. - -Required Headers: n/a - -Arguments: -: * `domain` - - ``` title="Example request (for gelbooru.com)" - /manage_cookies/get_cookies?domain=gelbooru.com - ``` - - -Response: -: A JSON Object listing all the cookies for that domain in \[ name, value, domain, path, expires \] format. -```json title="Example response" -{ - "cookies" : [ - ["__cfduid", "f1bef65041e54e93110a883360bc7e71", ".gelbooru.com", "/", 1596223327], - ["pass_hash", "0b0833b797f108e340b315bc5463c324", "gelbooru.com", "/", 1585855361], - ["user_id", "123456", "gelbooru.com", "/", 1585855361] - ] -} -``` - - Note that these variables are all strings except 'expires', which is either an integer timestamp or _null_ for session cookies. - - This request will also return any cookies for subdomains. The session system in hydrus generally stores cookies according to the second-level domain, so if you request for specific.someoverbooru.net, you will still get the cookies for someoverbooru.net and all its subdomains. - -### **POST `/manage_cookies/set_cookies`** { id="manage_cookies_set_cookies" } - -Set some new cookies for the client. This makes it easier to 'copy' a login from a web browser or similar to hydrus if hydrus's login system can't handle the site yet. - -Restricted access: -: YES. Manage Cookies permission needed. - -Required Headers: -: - * `Content-Type`: application/json - -Arguments (in JSON): -: - * `cookies`: (a list of cookie rows in the same format as the GET request above) - -```json title="Example request body" -{ - "cookies" : [ - ["PHPSESSID", "07669eb2a1a6e840e498bb6e0799f3fb", ".somesite.com", "/", 1627327719], - ["tag_filter", "1", ".somesite.com", "/", 1627327719] - ] -} -``` - -You can set 'value' to be null, which will clear any existing cookie with the corresponding name, domain, and path (acting essentially as a delete). - -Expires can be null, but session cookies will time-out in hydrus after 60 minutes of non-use. - -### **POST `/manage_headers/set_user_agent`** { id="manage_headers_set_user_agent" } - -This sets the 'Global' User-Agent for the client, as typically editable under _network->data->manage http headers_, for instance if you want hydrus to appear as a specific browser associated with some cookies. - -Restricted access: -: YES. Manage Cookies permission needed. - -Required Headers: -: - * `Content-Type`: application/json - -Arguments (in JSON): -: - * `user-agent`: (a string) - -```json title="Example request body" -{ - "user-agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0" -} -``` - -Send an empty string to reset the client back to the default User-Agent, which should be `Mozilla/5.0 (compatible; Hydrus Client)`. - -## Managing Pages - -This refers to the pages of the main client UI. - -### **GET `/manage_pages/get_pages`** { id="manage_pages_get_pages" } - -_Get the page structure of the current UI session._ - -Restricted access: -: YES. Manage Pages permission needed. - -Required Headers: n/a - -Arguments: n/a - - -Response: -: A JSON Object of the top-level page 'notebook' (page of pages) detailing its basic information and current sub-pages. Page of pages beneath it will list their own sub-page lists. -```json title="Example response" -{ - "pages" : { - "name" : "top pages notebook", - "page_key" : "3b28d8a59ec61834325eb6275d9df012860a1ecfd9e1246423059bc47fb6d5bd", - "page_type" : 10, - "selected" : true, - "pages" : [ - { - "name" : "files", - "page_key" : "d436ff5109215199913705eb9a7669d8a6b67c52e41c3b42904db083255ca84d", - "page_type" : 6, - "selected" : false - }, - { - "name" : "thread watcher", - "page_key" : "40887fa327edca01e1d69b533dddba4681b2c43e0b4ebee0576177852e8c32e7", - "page_type" : 9, - "selected" : false - }, - { - "name" : "pages", - "page_key" : "2ee7fa4058e1e23f2bd9e915cdf9347ae90902a8622d6559ba019a83a785c4dc", - "page_type" : 10, - "selected" : true, - "pages" : [ - { - "name" : "urls", - "page_key" : "9fe22cb760d9ee6de32575ed9f27b76b4c215179cf843d3f9044efeeca98411f", - "page_type" : 7, - "selected" : true - }, - { - "name" : "files", - "page_key" : "2977d57fc9c588be783727bcd54225d577b44e8aa2f91e365a3eb3c3f580dc4e", - "page_type" : 6, - "selected" : false - } - ] - } - ] - } -} -``` - - The page types are as follows: - - * 1 - Gallery downloader - * 2 - Simple downloader - * 3 - Hard drive import - * 5 - Petitions (used by repository janitors) - * 6 - File search - * 7 - URL downloader - * 8 - Duplicates - * 9 - Thread watcher - * 10 - Page of pages - - The top page of pages will always be there, and always selected. 'selected' means which page is currently in view and will propagate down other page of pages until it terminates. It may terminate in an empty page of pages, so do not assume it will end on a 'media' page. - - The 'page_key' is a unique identifier for the page. It will stay the same for a particular page throughout the session, but new ones are generated on a client restart or other session reload. - -### **GET `/manage_pages/get_page_info`** { id="manage_pages_get_page_info" } - -_Get information about a specific page._ - -!!! warning "Under Construction" - This is under construction. The current call dumps a ton of info for different downloader pages. Please experiment in IRL situations and give feedback for now! I will flesh out this help with more enumeration info and examples as this gets nailed down. POST commands to alter pages (adding, removing, highlighting), will come later. - -Restricted access: -: YES. Manage Pages permission needed. - -Required Headers: n/a - -Arguments: -: - * `page_key`: (hexadecimal page\_key as stated in [/manage\_pages/get\_pages](#manage_pages_get_pages)) - * `simple`: true or false (optional, defaulting to true) - - ``` title="Example request" - /manage_pages/get_page_info?page_key=aebbf4b594e6986bddf1eeb0b5846a1e6bc4e07088e517aff166f1aeb1c3c9da&simple=true - ``` - -Response description -: A JSON Object of the page's information. At present, this mostly means downloader information. -```json title="Example response with simple = true" -{ - "page_info" : { - "name" : "threads", - "page_key" : "aebbf4b594e6986bddf1eeb0b5846a1e6bc4e07088e517aff166f1aeb1c3c9da", - "page_type" : 3, - "management" : { - "multiple_watcher_import" : { - "watcher_imports" : [ - { - "url" : "https://someimageboard.net/m/123456", - "watcher_key" : "cf8c3525c57a46b0e5c2625812964364a2e801f8c49841c216b8f8d7a4d06d85", - "created" : 1566164269, - "last_check_time" : 1566164272, - "next_check_time" : 1566174272, - "files_paused" : false, - "checking_paused" : false, - "checking_status" : 0, - "subject" : "gundam pictures", - "imports" : { - "status" : "4 successful (2 already in db)", - "simple_status" : "4", - "total_processed" : 4, - "total_to_process" : 4 - }, - "gallery_log" : { - "status" : "1 successful", - "simple_status" : "1", - "total_processed" : 1, - "total_to_process" : 1 - } - }, - { - "url" : "https://someimageboard.net/a/1234", - "watcher_key" : "6bc17555b76da5bde2dcceedc382cf7d23281aee6477c41b643cd144ec168510", - "created" : 1566063125, - "last_check_time" : 1566063133, - "next_check_time" : 1566104272, - "files_paused" : false, - "checking_paused" : true, - "checking_status" : 1, - "subject" : "anime pictures", - "imports" : { - "status" : "124 successful (22 already in db), 2 previously deleted", - "simple_status" : "124", - "total_processed" : 124, - "total_to_process" : 124 - }, - "gallery_log" : { - "status" : "3 successful", - "simple_status" : "3", - "total_processed" : 3, - "total_to_process" : 3 - } - } - ] - }, - "highlight" : "cf8c3525c57a46b0e5c2625812964364a2e801f8c49841c216b8f8d7a4d06d85" - } - }, - "media" : { - "num_files" : 4 - } -} -``` - - As you can see, even the 'simple' mode can get very large. Imagine that response for a page watching 100 threads! Turning simple mode off will display every import item, gallery log entry, and all hashes in the media (thumbnail) panel. - - For this first version, the five importer pages--hdd import, simple downloader, url downloader, gallery page, and watcher page--all give rich info based on their specific variables. The first three only have one importer/gallery log combo, but the latter two of course can have multiple. The "imports" and "gallery_log" entries are all in the same data format. - - -### **POST `/manage_pages/add_files`** { id="manage_pages_add_files" } - -_Add files to a page._ - -Restricted access: -: YES. Manage Pages permission needed. - -Required Headers: -: - * `Content-Type`: application/json - -Arguments (in JSON): -: - * `page_key`: (the page key for the page you wish to add files to) - * [files](#parameters_files) - -The files you set will be appended to the given page, just like a thumbnail drag and drop operation. The page key is the same as fetched in the [/manage\_pages/get\_pages](#manage_pages_get_pages) call. - -```json title="Example request body" -{ - "page_key" : "af98318b6eece15fef3cf0378385ce759bfe056916f6e12157cd928eb56c1f18", - "file_ids" : [123, 124, 125] -} -``` - -Response: -: 200 with no content. If the page key is not found, this will 404. - -### **POST `/manage_pages/focus_page`** { id="manage_pages_focus_page" } - -_'Show' a page in the main GUI, making it the current page in view. If it is already the current page, no change is made._ - -Restricted access: -: YES. Manage Pages permission needed. - -Required Headers: -: - * `Content-Type`: application/json - -Arguments (in JSON): -: - * `page_key`: (the page key for the page you wish to show) - -The page key is the same as fetched in the [/manage\_pages/get\_pages](#manage_pages_get_pages) call. - -```json title="Example request body" -{ - "page_key" : "af98318b6eece15fef3cf0378385ce759bfe056916f6e12157cd928eb56c1f18" -} -``` - -Response: -: 200 with no content. If the page key is not found, this will 404. - - -### **POST `/manage_pages/refresh_page`** { id="manage_pages_refresh_page" } - -_Refresh a page in the main GUI. Like hitting F5 in the client, this obviously makes file search pages perform their search again, but for other page types it will force the currently in-view files to be re-sorted._ - -Restricted access: -: YES. Manage Pages permission needed. - -Required Headers: -: - * `Content-Type`: application/json - -Arguments (in JSON): -: - * `page_key`: (the page key for the page you wish to refresh) - -The page key is the same as fetched in the [/manage\_pages/get\_pages](#manage_pages_get_pages) call. If a file search page is not set to 'searching immediately', a 'refresh' command does nothing. - -```json title="Example request body" -{ - "page_key" : "af98318b6eece15fef3cf0378385ce759bfe056916f6e12157cd928eb56c1f18" -} -``` - -Response: -: 200 with no content. If the page key is not found, this will 404. - -## Managing the Database - -### **POST `/manage_database/lock_on`** { id="manage_database_lock_on" } - -_Pause the client's database activity and disconnect the current connection._ - -Restricted access: -: YES. Manage Database permission needed. - -Arguments: None - -This is a hacky prototype. It commands the client database to pause its job queue and release its connection (and related file locks and journal files). This puts the client in a similar position as a long VACUUM command--it'll hang in there, but not much will work, and since the UI async code isn't great yet, the UI may lock up after a minute or two. If you would like to automate database backup without shutting the client down, this is the thing to play with. - -This should return pretty quick, but it will wait up to five seconds for the database to actually disconnect. If there is a big job (like a VACUUM) current going on, it may take substantially longer to finish that up and process this STOP command. You might like to check for the existence of a journal file in the db dir just to be safe. - -As long as this lock is on, all Client API calls except the unlock command will return 503. (This is a decent way to test the current lock status, too) - -### **POST `/manage_database/lock_off`** { id="manage_database_lock_off" } - -_Reconnect the client's database and resume activity._ - -Restricted access: -: YES. Manage Database permission needed. - -Arguments: None - -This is the obvious complement to the lock. The client will resume processing its job queue and will catch up. If the UI was frozen, it should free up in a few seconds, just like after a big VACUUM. - -### **GET `/manage_database/mr_bones`** { id="manage_database_mr_bones" } - -_Get the data from help->how boned am I?. This is a simple Object of numbers just for hacky advanced purposes if you want to build up some stats in the background. The numbers are the same as the dialog shows, so double check that to confirm what means what._ - -Restricted access: -: YES. Manage Database permission needed. - -Arguments: None - -```json title="Example response" -{ - "boned_stats" : { - "num_inbox" : 8356, - "num_archive" : 229, - "num_deleted" : 7010, - "size_inbox" : 7052596762, - "size_archive" : 262911007, - "size_deleted" : 13742290193, - "earliest_import_time" : 1451408539, - "total_viewtime" : [3280, 41621, 2932, 83021], - "total_alternate_files" : 265, - "total_duplicate_files" : 125, - "total_potential_pairs" : 3252 - } -} -``` diff --git a/docs/getting_started_importing.md b/docs/getting_started_importing.md index ca82a76d..fedee9c9 100644 --- a/docs/getting_started_importing.md +++ b/docs/getting_started_importing.md @@ -19,7 +19,7 @@ Drag-and-drop one or more folders or files into Hydrus. This will open the `import files` window. Here you can add files or folders, or delete files from the import queue. Let Hydrus parse what it will update and then look over the options. By default the option to delete original files after succesful import (if it's ignored for any reason or already present in Hydrus for example) is not checked, activate on your own risk. In `file import options` you can find some settings for minimum and maximum file size, resolution, and whether to import previously deleted files or not. From here there's two options: `import now` which will just import as is, and `add tags before import >>` which lets you set up some rules to add tags to files on import. -Examples are keeping filename as a tag, add folders as tag (useful if you have some sort of folder based organisation scheme), or load tags from an accompanying .txt file generated by some other program. +Examples are keeping filename as a tag, add folders as tag (useful if you have some sort of folder based organisation scheme), or load tags from an [accompanying text file](advanced_sidecars.md) generated by some other program. Once you're done click apply (or `import now`) and Hydrus will start processing the files. Exact duplicates are not imported so if you had dupes spread out you will end up with only one file in the end. If files *look* similar but Hydrus imports both then that's a job for the [dupe filter](duplicates.md) as there is some difference even if you can't tell it by eye. A common one is compression giving files with different file sizes, but otherwise looking identical or files with extra meta data baked into them. @@ -41,7 +41,7 @@ If you use a drag and drop to open a file inside an image editing program, remem You can also copy the files by right-clicking and going down `share -> copy -> files` and then pasting the files where you want them. ### Export -You can also export files with tags, either in filename or as a side-car .txt file by right-clicking and going down `share -> export -> files`. Have a look at the settings and then press `export`. +You can also export files with tags, either in filename or as a [sidecar file](advanced_sidecars.md) by right-clicking and going down `share -> export -> files`. Have a look at the settings and then press `export`. You can create folders to export files into by using backslashes on Windows (`\`) and slashes on Linux (`/`) in the filename. This can be combined with the patterns listed in the pattern shortcut button dropdown. As example `[series]\{filehash}` will export files into folders named after the `series:` namespaced tags on the files, all files tagged with one series goes into one folder, files tagged with another series goes into another folder as seen in the image below. ![](images/export_files.png) @@ -56,12 +56,12 @@ Under `file -> import and export folders` you'll find options for setting up aut ### Import folders ![](images/import_folder.png) -To import tags you have to add a tag service under the `filename tagging` section. +Like with a manual import, if you wish you can import tags by parsing filenames or [loading sidecars](advanced_sidecars.md). ### Export folders ![](images/export_folder.png) -You can currently not export tags in a .txt file with export folder liks you can when doing normal exports unfortunately. +Like with manual export, you can set the filenames using a tag pattern, and you can [export to sidecars](advanced_sidecars.md) too. ## Importing and exporting tags While you can import and export tags together with images sometimes you just don't want to deal with the files. diff --git a/docs/images/sidecars_example_json_export.png b/docs/images/sidecars_example_json_export.png new file mode 100644 index 0000000000000000000000000000000000000000..81aaea71f738a631cacaba38fa387d3904b3dc1e GIT binary patch literal 17481 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

wGh@nlB{OI|s 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

E`DJ=pC;r`Tru?)Uv!c`qFf+BGH!~RyU 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 literal 0 HcmV?d00001 diff --git a/docs/images/sidecars_example_manual_export.png b/docs/images/sidecars_example_manual_export.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5dacd4391545b3bc0ba60d2bc0e181352663df GIT binary patch literal 94272 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 }k`{fBiPR21gz8A5e@@9LU#y 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 literal 0 HcmV?d00001 diff --git a/docs/images/sidecars_example_manual_import.png b/docs/images/sidecars_example_manual_import.png new file mode 100644 index 0000000000000000000000000000000000000000..7be19caca3690ae18370896f4ff620ae6a43e110 GIT binary patch literal 125535 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�VdBI&&t#gwx&`_rm|9y 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 Tf4S}h99umr6h=m(OuT># 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 literal 0 HcmV?d00001 diff --git a/docs/old_changelog.html b/docs/old_changelog.html index fa034c36..65052283 100644 --- a/docs/old_changelog.html +++ b/docs/old_changelog.html @@ -34,6 +34,81 @@

changelog

    +
  • +

    version 514

    +
      +
    • downloaders

    • +
    • twitter took down the API we were using, breaking all our nice twitter downloaders! argh!
    • +
    • a user has figured out a basic new downloader that grabs the tweets amongst the first twenty tweets-and-retweets of an account. yes, only the first twenty max, and usually fewer. because this is a big change, the client will ask about it when you update. if you have some complicated situation where you are working on the old default twitter downloaders and don't want them deleted, you can select 'no' on the dialog it throws up, but everyone else wants to say 'yes'. then check your twitter subs: make sure they moved to the new downloader, and you probably want to make them check more frequently too.
    • +
    • given the rate of changes at twitter, I think we can expect more changes and blocks in future. I don't know whether nitter will be viable alternative, so if the artists you like end up on a nice simple booru _anywhere_, I strongly recommend just moving there. twitter appears to be explicitly moving to non-third-party-friendly
    • +
    • thanks to a user's work, the 'danbooru - get webm ugoira' parser is fixed!
    • +
    • thanks to a user's work, the deviant art parser is updated to get the highest res image in more situations!
    • +
    • thanks to a user's work, the pixiv downloader now gets the artist note, in japanese (and translated, if there is one), and a 'medium:ai generated' tag!
    • +
    • sidecars

    • +
    • I wrote some sidecar help here! https://hydrusnetwork.github.io/hydrus/advanced_sidecars.html
    • +
    • when the client parses files for import, the 'does this look like a sidecar?' test now also checks that the base component of the base filename (e.g. 'Image123' from 'Image123.jpg.txt') actually appears in the list of non-txt/json/xml ext files. a random yo.txt file out of nowhere will now be inspected in case it is secretly a jpeg again, for good or ill
    • +
    • when you drop some files on the client, the number of files skipped because they looked like sidecars is now stated in the status label
    • +
    • fixed a typo bug that meant tags imported from sidecars were not being properly cleaned, despite preview appearance otherwise, for instance ':)', which in hydrus needs to be secretly stored as '::)' was being imported as ')'
    • +
    • as a special case, tags that in hydrus are secretly '::)' will be converted to ':)' on export to sidecar too, the inverse of the above problem. there may be some other tag cleaning quirks to undo here, so let me know what you run into
    • +
    • related tags overhaul

    • +
    • the 'related tags' suggestion system, turned on under _options->tag suggestions_, has several changes, including some prototype tech I'd love feedback on
    • +
    • first off, there are two new search buttons, 'new 1' and 'new 2' ('2' is available on repositories only).. these use an upgraded statistical search and scoring system that a user worked on and sent in. I have butchered his specific namespace searching system to something more general/flexible and easy for me to maintain, but it works better and more comprehensibly than my old method! give it a go and let me know how each button does--the first one will be fast but less useful on the PTR, the second will be slower but generally give richer results (although it cannot do tags with too-high count)
    • +
    • the new search routine works on multiple files, so 'related tags' now shows on tag dialogs launched from a selection of thumbnails!
    • +
    • also, all the related search buttons now search any selection of tags you make!!! so if you can't remember that character's name, just click on the series or another character they are often with and hit the search, and you should get a whole bunch appear
    • +
    • I am going to keep working on this in the future. the new buttons will become the only buttons, I'll try and mitigate the prototype search limitations, add some cancel tech, move to a time-based search length like the current buttons, and I'll add more settings, including for filtering so we aren't looking up related tags for 'page:x' and so on. I'm interested in knowing how you get on with IRL data. are there too many recommendations (is the tolerance too high?)? is the sorting good (is the stuff at the top relevant or often just noise?)?
    • +
    • misc

    • +
    • all users can now copy their service keys (which are a technical non-changing hex identifier for your client's services) from the review services window--advanced mode is no longer needed. this may be useful as the client api transitions to service keys
    • +
    • when a job in the downloader search log generates new jobs (e.g. fetches the next page), the new job(s) are now inserted after the parent. previously, they were appended to the end of the list. this changes how ngugs operate, converting their searches from interleaved to sequential!
    • +
    • restarting search log jobs now also places the new job after the restarted job
    • +
    • when you create a new export folder, if you have default metadata export sidecar settings from a previous manual file export, the program now asks if you want those for the new export folder or an empty list. previously, it just assigned the saved default, which could be jarring if it was saved from ages ago
    • +
    • added a migration guide to the running from source help. also brushed up some language and fixed a bunch of borked title weights in that document
    • +
    • the max initial and periodic file limits in subscriptions is now 50k when in advanced mode. I can't promise that would be nice though!
    • +
    • the file history chart no longer says that inbox and delete time tracking are new
    • +
    • misc fixes

    • +
    • fixed a cursor type detection test that was stopping the cursor from hiding immediately when you do a media viewer drag in Qt6
    • +
    • fixed an issue where 'clear deletion record' calls were not deleting from the newer 'all my files' domain. the erroneous extra records will be searched for and scrubbed on update
    • +
    • fixed the issue where if you had the new 'unnamespaced input gives (any namespace) wildcard results' search option on, you couldn't add any novel tags in WRITE autocomplete contexts like 'manage tags'!!! it could only offer the automatically converted wildcard tags as suggested input, which of course aren't appropriate for a WRITE context. the way I ultimately fixed this was horrible; the whole thing needs more work to deal with clever logic like this better, so let me know if you get any more trouble here
    • +
    • I think I fixed an infinite hang when trying to add certain siblings in manage tag siblings. I believe this was occuring when the dialog was testing if the new pair would create a loop when the sibling structure already contains a loop. now it throws up a message and breaks the test
    • +
    • fixed an issue where certain system:filetype predicates would spawn apparent duplicates of themselves instead of removing on double-click. images+audio+video+swf+pdf was one example. it was a 'all the image types' vs 'list of (all the) image types' conversion/comparison/sorting issue
    • +
    • client api

    • +
    • **this is later than I expected, but as was planned last year, I am clearing up several obsolete parameters and data structures this week. mostly it is bad service name-identification that seemed simple or flexible to support but just added maintenance debt, induced bad implementation practises, and hindered future expansions. if you have a custom api script, please read on--and if you have not yet moved to the alternatives, do so before updating!**
    • +
    • **all `...service_name...` parameters are officially obsolete! they will still work via some legacy hacks, so old scripts shouldn't break, but they are no longer documented. please move to the `...service_key...` alternates as soon as reasonably possible (check out `/get_services` if you need to learn about service keys)**
    • +
    • **`/add_tags/get_tag_services` is removed! use `/get_services` instead!**
    • +
    • **`hide_service_names_tags`, previously made default true, is removed and its data structures `service_names_to_statuses_to_...` are also gone! move to the new `tags` structure.**
    • +
    • **`hide_service_keys_tags` is now default true. it will be removed in 4 weeks or so. same deal as with `service_names_to_statuses_to_...`--move to `tags`**
    • +
    • **`system_inbox` and `system_archive` are removed from `/get_files/search_files`! just use 'system:inbox/archive' in the tags list**
    • +
    • **the 'set_file_relationships' command from last week has been reworked to have a nicer Object parameter with a new name. please check the updated help!** normally I wouldn't change something so quick, but we are still in early prototype, so I'm ok shifting it (and the old method still works lmao, but I'll clear that code out in a few weeks, so please move over--the Object will be much nicer to expand in future, which I forgot about in v513)
    • +
    • many Client API commands now support modern file domain objects, meaning you can search a UNION of file services and 'deleted-from' file services. affected commands are

    • +
    • * /add_files/delete_files
    • +
    • * /add_files/undelete_files
    • +
    • * /add_tags/search_tags
    • +
    • * /get_files/search_files
    • +
    • * /manage_file_relationships/get_everything
    • +
    • a new `/get_service` call now lets you ask about an individual service by service name or service key, basically a parameterised /get_services
    • +
    • the `/manage_pages/get_pages` and `/manage_pages/get_page_info` calls now give the `page_state`, a new enum that says if the page is ready, initialised, searching, or search-cancelled
    • +
    • to reduce duplicate argument spam, the client api help now specifies the complicated 'these files' and now 'this file domain' arguments into sub-sections, and the commands that use them just point to the subsections. check it out--it makes sense when you look at it.
    • +
    • `/add_tags/add_tags` now raises 400 if you give an invalid content action (e.g. pending to a local tag service). previously it skipped these rows silently
    • +
    • added and updated unit tests and help for the above changes
    • +
    • client api version is now 41
    • +
    • boring optimisation

    • +
    • when you are looking at a search log or file log, if entries are added, removed, or moved around, all the log entries that have changed row # now update (previously it just sent a redraw signal for the new rows, not the second-order affected rows that were shuffled up/down. many access routines for these logs are sped up
    • +
    • file log status checking is completely rewritten. the ways it searches, caches and optimises the 'which is the next item with x status' queues is faster and requires far less maintenance. large import queues have less overhead, so the in and outs of general download work should scale up much better now
    • +
    • the main data cache that stores rendered images, image tiles, and thumbnails now maintains itself far more efficiently. there was a hellish O(n) overhead when adding or removing an item which has been reduced to constant time. this gonk was being spammed every few minutes during normal memory maintenance, when hundreds of thumbs can be purged at once. clients with tens of thousands of thumbnails in memory will maintain that list far more smoothly
    • +
    • physical file delete is now more efficient, requiring far fewer hard drive hits to delete a media file. it is also far less aggressive, with a new setting in _options->files and trash_ that sets how long to wait between individual file deletes, default 250ms. before, it was full LFG mode with minor delays every hundred/thousand jobs, and since it takes a write lock, it was lagging out thumbnail load when hitting a lot of work. the daemon here also shuts down faster if caught working during program shut down
    • +
    • boring code cleanup

    • +
    • refactored some parsing routines to be more flexible
    • +
    • added some more dictionary and enum type testing to the client api parameter parsing routines. error messages should be better!
    • +
    • improved how `/add_tags/add_tags` parsing works. ensuring both access methods check all types and report nicer errors
    • +
    • cleaned up the `/search_files/file_metadata` call's parsing, moving to the new generalised method and smoothing out some old code flow. it now checks hashes against the last search, too
    • +
    • cleaned up `/manage_pages/add_files` similarly
    • +
    • cleaned up how tag services are parsed and their errors reported in the client api
    • +
    • the client api is better about processing the file identifiers you give it in the same order you gave
    • +
    • fixed bad 'potentials_search_type'/'search_type' inconsistency in the client api help examples
    • +
    • obviously a bunch of client api unit test and help cleanup to account for the obsolete stuff and various other changes here
    • +
    • updated a bunch of the client api unit tests to handle some of the new parsing
    • +
    • fixed the remaining 'randomly fail due to complex counting logic' potential count unit tests. turns out there were like seven more of them
    • +
    +
  • version 513

      diff --git a/docs/running_from_source.md b/docs/running_from_source.md index 11ccc472..6d3fb49e 100644 --- a/docs/running_from_source.md +++ b/docs/running_from_source.md @@ -186,7 +186,7 @@ The first start will take a little longer. It will operate just like a normal bu If you want to redirect your database or use any other launch arguments, then copy 'client.command' to 'client-user.command' and edit it, inserting your desired db path. Run this instead of 'client.command'. New `git pull` commands will not affect 'client-user.command'. -## Simple Updating Guide +### Simple Updating Guide To update, you do the same thing as for the extract builds. @@ -195,11 +195,33 @@ To update, you do the same thing as for the extract builds. If you get a library version error when you try to boot, run the venv setup again. It is worth doing this anyway, every now and then, just to stay up to date. -## doing it manually { id="what_you_need" } +### Migrating from an Existing Install + +Many users start out using one of the official built releases and decide to move to source. There is lots of information [here](database_migration.md) about how to migrate the database, but for your purposes, the simple method is this: + +**If you never moved your database to another place and do not use -d/--db_dir launch parameter** + +1. Follow the above guide to get the source install working in a new folder on a fresh database +2. **MAKE A BACKUP OF EVERYTHING** +3. Delete everything from the source install's `db` directory. +4. Move your built release's entire `db` directory to the source. +5. Run your source release again--it should load your old db no problem! +6. Update your backup routine to point to the new source install location. + +**If you moved your database to another location and use the -d/--db_dir launch parameter** + +1. Follow the above guide to get the source install working in a new folder on a fresh database (without -db_dir) +2. **MAKE A BACKUP OF EVERYTHING** +3. Just to be neat, delete the .db files, .log files, and client_files folder from the source install's `db` directory. +4. Run the source install with --db_dir just as you would the built executable--it should load your old db no problem! + +## Doing it Yourself { id="what_you_need" } _This is for advanced users only._ -Inside the extract should be client.py and server.py. You will be treating these basically the same as the 'client' and 'server' executables--you should be able to launch them the same way and they take the same launch parameters as the exes. +_If you have never used python before, do not try this. If the easy setup scripts failed for you and you don't know what happened, please contact hydev before trying this, as the thing that went wrong there will probably go much more wrong here._ + +You can also set up the environment yourself. Inside the extract should be client.py and server.py. You will be treating these basically the same as the 'client' and 'server' executables--with the right environment, you should be able to launch them the same way and they take the same launch parameters as the exes. Hydrus needs a whole bunch of libraries, so let's now set your python up. I **strongly** recommend you create a virtual environment. It is easy and doesn't mess up your system python. @@ -210,14 +232,14 @@ To create a new venv environment: * Open a terminal at your hydrus extract folder. If `python3` doesn't work, use `python`. * `python3 -m pip install virtualenv` (if you need it) * `python3 -m venv venv` -* `source venv/bin/activate` +* `source venv/bin/activate` (`CALL venv\Scripts\activate.bat` in Windows cmd) * `python -m pip install --upgrade pip` * `python -m pip install --upgrade wheel` !!! info "venvs" - That `source venv/bin/activate` line turns on your venv. You should see your terminal note you are now in it. A venv is an isolated environment of python that you can install modules to without worrying about breaking something system-wide. **Ideally, you do not want to install python modules to your system python.** + That `source venv/bin/activate` line turns on your venv. You should see your terminal prompt note you are now in it. A venv is an isolated environment of python that you can install modules to without worrying about breaking something system-wide. **Ideally, you do not want to install python modules to your system python.** - This activate line will be needed every time you alter your venv or run the `client.py`/`server.py` files. You can easily tuck this venv activation line into a launch script--check the easy setup files for examples. + This activate line will be needed every time you alter your venv or run the `client.py`/`server.py` files. You can easily tuck this into a launch script--check the easy setup files for examples. On Windows Powershell, the command is `.\venv\Scripts\activate`, but you may find the whole deal is done much easier in cmd than Powershell. When in Powershell, just type `cmd` to get an old fashioned command line. In cmd, the launch command is just `venv\scripts\activate.bat`, no leading period. @@ -227,9 +249,9 @@ To create a new venv environment: python -m pip install -r requirements.txt ``` -If you need different versions of libraries, check the cut-up requirements.txts the 'advanced' easy-setup uses in `install_dir/static/requirements/advanced`. Check and compare their contents to the main requirements.txt to see what is going on. +If you need different versions of libraries, check the cut-up requirements.txts the 'advanced' easy-setup uses in `install_dir/static/requirements/advanced`. Check and compare their contents to the main requirements.txt to see what is going on. You'll likely need the newer OpenCV on Python 3.10, for instance. -## Qt { id="qt" } +### Qt { id="qt" } Qt is the UI library. You can run PySide2, PySide6, PyQt5, or PyQt6. A wrapper library called `qtpy` allows this. The default is PySide6, but if it is missing, qtpy will fall back to an available alternative. For PyQt5 or PyQt6, you need an extra Chart module, so go: @@ -245,17 +267,15 @@ If you want to set QT_API in a batch file, do this: `set QT_API=pyqt6` -If you run Windows 8.1 or Ubuntu 18.04, you cannot run Qt6. Please try PySide2 or PyQt5. +If you run <= Windows 8.1 or Ubuntu 18.04, you cannot run Qt6. Try PySide2 or PyQt5. -## mpv support { id="mpv" } +### mpv { id="mpv" } MPV is optional and complicated, but it is great, so it is worth the time to figure out! -As well as the python wrapper, 'python-mpv' (which is in the requirements.txt), you also need the underlying library. This is _not_ mpv the program, but 'libmpv', often called 'libmpv1'. +As well as the python wrapper, 'python-mpv' (which is in the requirements.txt), you also need the underlying dev library. This is _not_ mpv the program, but 'libmpv', often called 'libmpv1'. -For Windows, the dll builds are [here](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/), although getting a stable version can be difficult. Just put it in your hydrus base install directory. Check the links in the easy-setup guide above for good versions. - -You can also just grab the 'mpv-1.dll'/'mpv-2.dll' I bundle in my extractable Windows release. +For Windows, the dll builds are [here](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/), although getting a stable version can be difficult. Just put it in your hydrus base install directory. Check the links in the easy-setup guide above for good versions. You can also just grab the 'mpv-1.dll'/'mpv-2.dll' I bundle in my extractable Windows release. If you are on Linux, you can usually get 'libmpv1' like so: @@ -265,13 +285,11 @@ On macOS, you should be able to get it with `brew install mpv`, but you are like Hit _help->about_ to see your mpv status. If you don't have it, it will present an error popup box with more info. -## SQLite { id="sqlite" } +### SQLite { id="sqlite" } -If you can, update python's SQLite--it'll improve performance. +If you can, update python's SQLite--it'll improve performance. The SQLite that comes with stock python is usually quite old, so you'll get a significant boost in speed. In some python deployments, the built-in SQLite not compiled with neat features like Fast Text Search (FTS) that hydrus needs. -On Windows, get the 64-bit sqlite3.dll [here](https://www.sqlite.org/download.html), and just drop it in your base install directory. - -You can also just grab the 'sqlite3.dll' I bundle in my extractable Windows release. +On Windows, get the 64-bit sqlite3.dll [here](https://www.sqlite.org/download.html), and just drop it in your base install directory. You can also just grab the 'sqlite3.dll' I bundle in my extractable Windows release. You _may_ be able to update your SQLite on Linux or macOS with: @@ -279,27 +297,27 @@ You _may_ be able to update your SQLite on Linux or macOS with: * (activate your venv) * `python -m pip install pysqlite3` -But it isn't a big deal. +But as long as the program launches, it usually isn't a big deal. !!! warning "Extremely safe no way it can go wrong" If you want to update sqlite for your system python install, you can also drop it into `C:\Python38\DLLs` or wherever you have python installed. You'll be overwriting the old file, so make a backup if you want to (I have never had trouble updating like this, however). + + A user who made a Windows venv with Anaconda reported they had to replace the sqlite3.dll in their conda env at `~/.conda/envs//Library/bin/sqlite3.dll`. -## FFMPEG { id="ffmpeg" } +### FFMPEG { id="ffmpeg" } -If you don't have FFMPEG in your PATH and you want to import anything more fun than jpegs, you will need to put a static [FFMPEG](https://ffmpeg.org/) executable in your PATH or the `install_dir/bin` directory. [This](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z) should always point to a new build. +If you don't have FFMPEG in your PATH and you want to import anything more fun than jpegs, you will need to put a static [FFMPEG](https://ffmpeg.org/) executable in your PATH or the `install_dir/bin` directory. [This](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z) should always point to a new build for Windows. Alternately, you can just copy the exe from one of my extractable Windows releases. -Alternately, you can just copy the exe from one of my extractable Windows releases. +### Running It { id="running_it" } -## running it { id="running_it" } - -Once you have everything set up, client.py and server.py should look for and run off client.db and server.db just like the executables. You are looking at entering something like this into the terminal: +Once you have everything set up, client.py and server.py should look for and run off client.db and server.db just like the executables. You can use the 'client.bat/sh/command' scripts in the install dir or use them as inspiration for your own. In any case, you are looking at entering something like this into the terminal: ``` source venv/bin/activate python client.py ``` -This will look in the 'db' directory by default, but you can use the [launch arguments](launch_arguments.md) just like for the executables. For example, this could be your client-user.sh file: +This will use the 'db' directory for your database by default, but you can use the [launch arguments](launch_arguments.md) just like for the executables. For example, this could be your client-user.sh file: ``` #!/bin/bash @@ -308,13 +326,11 @@ source venv/bin/activate python client.py -d="/path/to/database" ``` -I develop hydrus on and am most experienced with Windows, so the program is more stable and reasonable on that. I do not have as much experience with Linux or macOS, but I still appreciate and will work on your Linux/macOS bug reports. +### Building these Docs -## Building the docs +When running from source you may want to [build the hydrus help docs](about_docs.md) yourself. You can also check the `setup_help` scripts in the install directory. -When running from source you may want to [build the hydrus help docs](about_docs.md) yourself. - -## building packages on windows { id="windows_build" } +### Building Packages on Windows { id="windows_build" } Almost everything you get through pip is provided as pre-compiled 'wheels' these days, but if you get an error about Visual Studio C++ when you try to pip something, you have two choices: @@ -335,13 +351,15 @@ Trust me, just do this, it will save a ton of headaches! _Update:_ On Windows 11, in 2023-01, I had trouble with the above. There's a couple '11' SDKs that installed ok, but the vcbuildtools stuff had unusual errors. I hadn't done this in years, so maybe they are broken for Windows 10 too! The good news is that a basic stock Win 11 install with Python 3.10 is fine getting everything on our requirements and even making a build without any extra compiler tech. -## additional windows info { id="additional_windows" } +### Additional Windows Info { id="additional_windows" } This does not matter much any more, but in the old days, building modules like lz4 and lxml was a complete nightmare, and hooking up Visual Studio was even more difficult. [This page](http://www.lfd.uci.edu/~gohlke/pythonlibs/) has a lot of prebuilt binaries--I have found it very helpful many times. I have a fair bit of experience with Windows python, so send me a mail if you need help. -## my code { id="my_code" } +## My Code { id="my_code" } + +I develop hydrus on and am most experienced with Windows, so the program is more stable and reasonable on that. I do not have as much experience with Linux or macOS, but I still appreciate and will work on your Linux/macOS bug reports. My coding style is unusual and unprofessional. Everything is pretty much hacked together. If you are interested in how things work, please do look through the source and ask me if you don't understand something. diff --git a/hydrus/client/ClientCaches.py b/hydrus/client/ClientCaches.py index 240a5ee5..9f453b0c 100644 --- a/hydrus/client/ClientCaches.py +++ b/hydrus/client/ClientCaches.py @@ -42,15 +42,17 @@ class DataCache( object ): if key not in self._keys_to_data: return - + + + ( data, size_estimate ) = self._keys_to_data[ key ] del self._keys_to_data[ key ] - self._RecalcMemoryUsage() + self._total_estimated_memory_footprint -= size_estimate if HG.cache_report_mode: - HydrusData.ShowText( 'Cache "{}" removing "{}". Current size {}.'.format( self._name, key, HydrusData.ConvertValueRangeToBytes( self._total_estimated_memory_footprint, self._cache_size ) ) ) + HydrusData.ShowText( 'Cache "{}" removing "{}", size "{}". Current size {}.'.format( self._name, key, HydrusData.ToHumanBytes( size_estimate ), HydrusData.ConvertValueRangeToBytes( self._total_estimated_memory_footprint, self._cache_size ) ) ) @@ -61,9 +63,27 @@ class DataCache( object ): self._Delete( deletee_key ) - def _RecalcMemoryUsage( self ): + def _GetData( self, key ): - self._total_estimated_memory_footprint = sum( ( data.GetEstimatedMemoryFootprint() for data in self._keys_to_data.values() ) ) + if key not in self._keys_to_data: + + raise Exception( 'Cache error! Looking for {}, but it was missing.'.format( key ) ) + + + self._TouchKey( key ) + + ( data, size_estimate ) = self._keys_to_data[ key ] + + new_estimate = data.GetEstimatedMemoryFootprint() + + if new_estimate != size_estimate: + + self._total_estimated_memory_footprint += new_estimate - size_estimate + + self._keys_to_data[ key ] = ( data, new_estimate ) + + + return data def _TouchKey( self, key ): @@ -99,19 +119,21 @@ class DataCache( object ): self._DeleteItem() - self._keys_to_data[ key ] = data + size_estimate = data.GetEstimatedMemoryFootprint() + + self._keys_to_data[ key ] = ( data, size_estimate ) + + self._total_estimated_memory_footprint += size_estimate self._TouchKey( key ) - self._RecalcMemoryUsage() - if HG.cache_report_mode: HydrusData.ShowText( 'Cache "{}" adding "{}" ({}). Current size {}.'.format( self._name, key, - HydrusData.ToHumanBytes( data.GetEstimatedMemoryFootprint() ), + HydrusData.ToHumanBytes( size_estimate ), HydrusData.ConvertValueRangeToBytes( self._total_estimated_memory_footprint, self._cache_size ) ) ) @@ -132,14 +154,7 @@ class DataCache( object ): with self._lock: - if key not in self._keys_to_data: - - raise Exception( 'Cache error! Looking for {}, but it was missing.'.format( key ) ) - - - self._TouchKey( key ) - - return self._keys_to_data[ key ] + return self._GetData( key ) @@ -149,9 +164,7 @@ class DataCache( object ): if key in self._keys_to_data: - self._TouchKey( key ) - - return self._keys_to_data[ key ] + return self._GetData( key ) else: @@ -180,6 +193,11 @@ class DataCache( object ): with self._lock: + while self._total_estimated_memory_footprint > self._cache_size: + + self._DeleteItem() + + while True: if len( self._keys_fifo ) == 0: diff --git a/hydrus/client/ClientConstants.py b/hydrus/client/ClientConstants.py index 0ab82859..4c1358d5 100644 --- a/hydrus/client/ClientConstants.py +++ b/hydrus/client/ClientConstants.py @@ -303,6 +303,11 @@ page_file_count_display_string_lookup = { PAGE_FILE_COUNT_DISPLAY_NONE : 'for no pages' } +PAGE_STATE_NORMAL = 0 +PAGE_STATE_INITIALISING = 1 +PAGE_STATE_SEARCHING = 2 +PAGE_STATE_SEARCHING_CANCELLED = 3 + SHUTDOWN_TIMESTAMP_VACUUM = 0 SHUTDOWN_TIMESTAMP_FATTEN_AC_CACHE = 1 SHUTDOWN_TIMESTAMP_DELETE_ORPHANS = 2 diff --git a/hydrus/client/ClientFiles.py b/hydrus/client/ClientFiles.py index 096f8106..3f3c02b7 100644 --- a/hydrus/client/ClientFiles.py +++ b/hydrus/client/ClientFiles.py @@ -51,8 +51,8 @@ regen_file_enum_to_str_lookup = { REGENERATE_FILE_DATA_JOB_REFIT_THUMBNAIL : 'regenerate thumbnail if incorrect size', REGENERATE_FILE_DATA_JOB_OTHER_HASHES : 'regenerate non-standard hashes', REGENERATE_FILE_DATA_JOB_DELETE_NEIGHBOUR_DUPES : 'delete duplicate neighbours with incorrect file extension', - REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_REMOVE_RECORD : 'if file is missing, remove record (no delete record)', - REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_DELETE_RECORD : 'if file is missing, remove record (leave delete record)', + REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_REMOVE_RECORD : 'if file is missing, remove record (leave no delete record)', + REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_DELETE_RECORD : 'if file is missing, remove record (leave a delete record)', REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_TRY_URL : 'if file is missing, then if has URL try to redownload', REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_TRY_URL_ELSE_REMOVE_RECORD : 'if file is missing, then if has URL try to redownload, else remove record', REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_LOG_ONLY : 'if file is missing, note it in log', @@ -185,6 +185,8 @@ def GetAllFilePaths( raw_paths, do_human_sort = True, clear_out_sidecars = True file_paths = [] + num_sidecars = 0 + paths_to_process = list( raw_paths ) while len( paths_to_process ) > 0: @@ -230,24 +232,49 @@ def GetAllFilePaths( raw_paths, do_human_sort = True, clear_out_sidecars = True HydrusData.HumanTextSort( file_paths ) + num_files_with_sidecars = len( file_paths ) + if clear_out_sidecars: exts = [ '.txt', '.json', '.xml' ] - def not_a_sidecar( p ): + def has_sidecar_ext( p ): if True in ( p.endswith( ext ) for ext in exts ): - return False + return True - return True + return False - file_paths = [ path for path in file_paths if not_a_sidecar( path ) ] + def get_base_prefix_component( p ): + + base_prefix = os.path.basename( p ) + + if '.' in base_prefix: + + base_prefix = base_prefix.split( '.', 1 )[0] + + + return base_prefix + + + # let's get all the 'Image123' in our 'path/to/Image123.jpg' list + all_non_ext_prefix_components = { get_base_prefix_component( file_path ) for file_path in file_paths if not has_sidecar_ext( file_path ) } + + def looks_like_a_sidecar( p ): + + # if we have Image123.txt, that's probably a sidecar! + return has_sidecar_ext( p ) and get_base_prefix_component( p ) in all_non_ext_prefix_components + + + file_paths = [ path for path in file_paths if not looks_like_a_sidecar( path ) ] - return file_paths + num_sidecars = num_files_with_sidecars - len( file_paths ) + + return ( file_paths, num_sidecars ) class ClientFilesManager( object ): @@ -259,7 +286,7 @@ class ClientFilesManager( object ): self._prefixes_to_locations = {} - self._new_physical_file_deletes = threading.Event() + self._physical_file_delete_wait = threading.Event() self._locations_to_free_space = {} @@ -1172,11 +1199,11 @@ class ClientFilesManager( object ): def DoDeferredPhysicalDeletes( self ): + wait_period = self._controller.new_options.GetInteger( 'ms_to_wait_between_physical_file_deletes' ) / 1000 + num_files_deleted = 0 num_thumbnails_deleted = 0 - pauser = HydrusData.BigJobPauser() - while not HG.started_shutdown: with self._rwlock.write: @@ -1190,9 +1217,18 @@ class ClientFilesManager( object ): if file_hash is not None: + media_result = self._controller.Read( 'media_result', file_hash ) + + expected_mime = media_result.GetMime() + try: - ( path, mime ) = self._LookForFilePath( file_hash ) + path = self._GenerateExpectedFilePath( file_hash, expected_mime ) + + if not os.path.exists( path ): + + ( path, actual_mime ) = self._LookForFilePath( file_hash ) + ClientPaths.DeletePath( path ) @@ -1200,7 +1236,7 @@ class ClientFilesManager( object ): except HydrusExceptions.FileMissingException: - pass + HydrusData.Print( 'Wanted to physically delete the "{}" file, with expected mime "{}", but it was not found!'.format( file_hash.hex(), expected_mime ) ) @@ -1214,6 +1250,10 @@ class ClientFilesManager( object ): num_thumbnails_deleted += 1 + else: + + HydrusData.Print( 'Wanted to physically delete the "{}" thumbnail, but it was not found!'.format( file_hash.hex() ) ) + self._controller.WriteSynchronous( 'clear_deferred_physical_delete', file_hash = file_hash, thumbnail_hash = thumbnail_hash ) @@ -1224,7 +1264,9 @@ class ClientFilesManager( object ): - pauser.Pause() + self._physical_file_delete_wait.wait( wait_period ) + + self._physical_file_delete_wait.clear() if num_files_deleted > 0 or num_thumbnails_deleted > 0: @@ -1335,11 +1377,6 @@ class ClientFilesManager( object ): return os.path.exists( path ) - def NotifyNewPhysicalFileDeletes( self ): - - self._new_physical_file_deletes.set() - - def Rebalance( self, job_key ): try: @@ -1502,7 +1539,7 @@ class ClientFilesManager( object ): def shutdown( self ): - self._new_physical_file_deletes.set() + self._physical_file_delete_wait.set() class FilesMaintenanceManager( object ): diff --git a/hydrus/client/ClientOptions.py b/hydrus/client/ClientOptions.py index 7a2e2122..0f0f3474 100644 --- a/hydrus/client/ClientOptions.py +++ b/hydrus/client/ClientOptions.py @@ -482,6 +482,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): self._dictionary[ 'integers' ][ 'human_bytes_sig_figs' ] = 3 + self._dictionary[ 'integers' ][ 'ms_to_wait_between_physical_file_deletes' ] = 250 + # self._dictionary[ 'keys' ] = {} diff --git a/hydrus/client/ClientSearch.py b/hydrus/client/ClientSearch.py index 52e6f5b0..2c69750b 100644 --- a/hydrus/client/ClientSearch.py +++ b/hydrus/client/ClientSearch.py @@ -1576,7 +1576,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ): if predicate_type == PREDICATE_TYPE_SYSTEM_MIME and value is not None: - value = tuple( ConvertSpecificFiletypesToSummary( value ) ) + value = tuple( sorted( ConvertSpecificFiletypesToSummary( value ) ) ) if predicate_type == PREDICATE_TYPE_OR_CONTAINER: @@ -1771,6 +1771,11 @@ class Predicate( HydrusSerialisable.SerialisableBase ): self._value = serialisable_value + if self._predicate_type == PREDICATE_TYPE_SYSTEM_MIME and self._value is not None: + + self._value = tuple( sorted( ConvertSpecificFiletypesToSummary( self._value ) ) ) + + if isinstance( self._value, list ): @@ -1863,7 +1868,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ): summary_mimes = ConvertSpecificFiletypesToSummary( specific_mimes ) - serialisable_value = tuple( summary_mimes ) + serialisable_value = tuple( sorted( summary_mimes ) ) new_serialisable_info = ( predicate_type, serialisable_value, inclusive ) @@ -3100,7 +3105,7 @@ class ParsedAutocompleteText( object ): return 'AC Tag Text: {}'.format( self.raw_input ) - def _GetSearchText( self, always_autocompleting: bool, force_do_not_collapse: bool = False, allow_unnamespaced_search_gives_any_namespace_wildcards: bool = True ) -> str: + def _GetSearchText( self, always_autocompleting: bool, force_do_not_collapse: bool = False, allow_auto_wildcard_conversion: bool = False ) -> str: text = CollapseWildcardCharacters( self.raw_content ) @@ -3114,7 +3119,7 @@ class ParsedAutocompleteText( object ): text = ConvertTagToSearchable( text ) - if allow_unnamespaced_search_gives_any_namespace_wildcards and self._tag_autocomplete_options.UnnamespacedSearchGivesAnyNamespaceWildcards(): + if allow_auto_wildcard_conversion and self._tag_autocomplete_options.UnnamespacedSearchGivesAnyNamespaceWildcards(): if ':' not in text: @@ -3149,12 +3154,12 @@ class ParsedAutocompleteText( object ): def GetAddTagPredicate( self ): - return Predicate( PREDICATE_TYPE_TAG, self.raw_content ) + return Predicate( PREDICATE_TYPE_TAG, self.raw_content, self.inclusive ) - def GetImmediateFileSearchPredicate( self ): + def GetImmediateFileSearchPredicate( self, allow_auto_wildcard_conversion ): - non_tag_predicates = self.GetNonTagFileSearchPredicates() + non_tag_predicates = self.GetNonTagFileSearchPredicates( allow_auto_wildcard_conversion ) if len( non_tag_predicates ) > 0: @@ -3166,7 +3171,7 @@ class ParsedAutocompleteText( object ): return tag_search_predicate - def GetNonTagFileSearchPredicates( self ): + def GetNonTagFileSearchPredicates( self, allow_auto_wildcard_conversion ): predicates = [] @@ -3182,7 +3187,7 @@ class ParsedAutocompleteText( object ): predicates.append( predicate ) - elif self.IsExplicitWildcard(): + elif self.IsExplicitWildcard( allow_auto_wildcard_conversion ): search_texts = [] @@ -3199,7 +3204,7 @@ class ParsedAutocompleteText( object ): for always_autocompleting in always_autocompleting_values: - search_texts.append( self._GetSearchText( always_autocompleting, allow_unnamespaced_search_gives_any_namespace_wildcards = allow_unnamespaced_search_gives_any_namespace_wildcards, force_do_not_collapse = True ) ) + search_texts.append( self._GetSearchText( always_autocompleting, allow_auto_wildcard_conversion = allow_unnamespaced_search_gives_any_namespace_wildcards, force_do_not_collapse = True ) ) @@ -3220,9 +3225,9 @@ class ParsedAutocompleteText( object ): return predicates - def GetSearchText( self, always_autocompleting: bool ): + def GetSearchText( self, always_autocompleting: bool, allow_auto_wildcard_conversion = True ): - return self._GetSearchText( always_autocompleting ) + return self._GetSearchText( always_autocompleting, allow_auto_wildcard_conversion = allow_auto_wildcard_conversion ) def GetTagAutocompleteOptions( self ): @@ -3232,7 +3237,7 @@ class ParsedAutocompleteText( object ): def IsAcceptableForTagSearches( self ): - search_text = self._GetSearchText( False ) + search_text = self._GetSearchText( False, allow_auto_wildcard_conversion = True ) if search_text == '': @@ -3267,7 +3272,7 @@ class ParsedAutocompleteText( object ): def IsAcceptableForFileSearches( self ): - search_text = self._GetSearchText( False ) + search_text = self._GetSearchText( False, allow_auto_wildcard_conversion = True ) if len( search_text ) == 0: @@ -3287,10 +3292,10 @@ class ParsedAutocompleteText( object ): return self.raw_input == '' - def IsExplicitWildcard( self ): + def IsExplicitWildcard( self, allow_auto_wildcard_conversion ): # user has intentionally put a '*' in - return '*' in self.raw_content or self._GetSearchText( False ).startswith( '*:' ) + return '*' in self.raw_content or self._GetSearchText( False, allow_auto_wildcard_conversion = allow_auto_wildcard_conversion ).startswith( '*:' ) def IsNamespaceSearch( self ): @@ -3300,9 +3305,9 @@ class ParsedAutocompleteText( object ): return SearchTextIsNamespaceFetchAll( search_text ) or SearchTextIsNamespaceBareFetchAll( search_text ) - def IsTagSearch( self ): + def IsTagSearch( self, allow_auto_wildcard_conversion ): - if self.IsEmpty() or self.IsExplicitWildcard() or self.IsNamespaceSearch(): + if self.IsEmpty() or self.IsExplicitWildcard( allow_auto_wildcard_conversion ) or self.IsNamespaceSearch(): return False diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py index 80b29a77..e4ead699 100644 --- a/hydrus/client/db/ClientDB.py +++ b/hydrus/client/db/ClientDB.py @@ -5158,6 +5158,246 @@ class DB( HydrusDB.HydrusDB ): return predicates + def _GetRelatedTagsNewOneTag( self, tag_display_type, file_service_id, tag_service_id, search_tag_id ): + + # a user provided the basic idea here + + # we are saying get me all the tags for all the hashes this tag has + # specifying namespace is critical to increase search speed, otherwise we actually are searching all tags for tags + # we also call this with single specific file domains to keep things fast + + # also this thing searches in fixed file domain to get fast + + # this table selection is hacky as anything, but simpler than GetMappingAndTagTables for now + + mappings_table_names = [] + + if file_service_id == self.modules_services.combined_file_service_id: + + ( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = ClientDBMappingsStorage.GenerateMappingsTableNames( tag_service_id ) + + mappings_table_names.extend( [ current_mappings_table_name, pending_mappings_table_name ] ) + + else: + + if tag_display_type == ClientTags.TAG_DISPLAY_ACTUAL: + + ( cache_current_display_mappings_table_name, cache_pending_display_mappings_table_name ) = ClientDBMappingsStorage.GenerateSpecificDisplayMappingsCacheTableNames( file_service_id, tag_service_id ) + + mappings_table_names.extend( [ cache_current_display_mappings_table_name, cache_pending_display_mappings_table_name ] ) + + else: + + statuses_to_table_names = self.modules_mappings_storage.GetFastestStorageMappingTableNames( file_service_id, tag_service_id ) + + mappings_table_names.extend( [ statuses_to_table_names[ HC.CONTENT_STATUS_CURRENT ], statuses_to_table_names[ HC.CONTENT_STATUS_PENDING ] ] ) + + + + results = collections.Counter() + + tags_table_name = self.modules_tag_search.GetTagsTableName( file_service_id, tag_service_id ) + + # while this searches pending and current tags, it does not cross-reference current and pending on the same file, oh well! + + for mappings_table_name in mappings_table_names: + + search_predicate = 'hash_id IN ( SELECT hash_id FROM {} WHERE tag_id = {} )'.format( mappings_table_name, search_tag_id ) + + query = 'SELECT tag_id, COUNT( * ) FROM {} CROSS JOIN {} USING ( tag_id ) WHERE {} GROUP BY subtag_id;'.format( mappings_table_name, tags_table_name, search_predicate ) + + results.update( dict( self._Execute( query ).fetchall() ) ) + + + return results + + + def _GetRelatedTagsNew( self, file_service_key, tag_service_key, search_tags, max_results = 100, concurrence_threshold = 0.04, total_search_tag_count_threshold = 500000 ): + + # a user provided the basic idea here + + if len( search_tags ) == 0: + + return [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = 'no search tags to work with!' ) ] + + + tag_display_type = ClientTags.TAG_DISPLAY_ACTUAL + + tag_service_id = self.modules_services.GetServiceId( tag_service_key ) + file_service_id = self.modules_services.GetServiceId( file_service_key ) + + if tag_display_type == ClientTags.TAG_DISPLAY_ACTUAL: + + search_tags = self.modules_tag_siblings.GetIdeals( tag_display_type, tag_service_key, search_tags ) + + # I had a thing here that added the parents, but it gave some whack results compared to what you expected + + + search_tag_ids_to_search_tags = self.modules_tags_local_cache.GetTagIdsToTags( tags = search_tags ) + + with self._MakeTemporaryIntegerTable( search_tag_ids_to_search_tags.keys(), 'tag_id' ) as temp_tag_id_table_name: + + search_tag_ids_to_total_counts = collections.Counter( { tag_id : current_count + pending_count for ( tag_id, current_count, pending_count ) in self.modules_mappings_counts.GetCountsForTags( tag_display_type, file_service_id, tag_service_id, temp_tag_id_table_name ) } ) + + + # + + search_tags = set() + + for ( search_tag_id, count ) in search_tag_ids_to_total_counts.items(): + + # pending only + if count == 0: + + continue + + + search_tags.add( search_tag_ids_to_search_tags[ search_tag_id ] ) + + + if len( search_tags ) == 0: + + return [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = 'not enough data in search domain' ) ] + + + # + + search_tag_ids_flat_sorted_ascending = sorted( search_tag_ids_to_total_counts.items(), key = lambda row: row[1] ) + + search_tags = set() + total_count = 0 + + # TODO: I think I would rather rework this into a time threshold thing like the old related tags stuff. + # so, should ditch the culling and instead make all the requests cancellable. just keep searching until we are out of time, then put results together + + # TODO: Another option as I vacillate on 'all my files' vs 'all known files' would be to incorporate that into the search timer + # do all my files first, then replace that with all known files results until we run out of time (only do this for repositories) + + # we don't really want to use '1girl' and friends as search tags here, since the search domain is so huge + # so, we go for the smallest count tags first. they have interesting suggestions + # searching all known files is gonkmode, so we curtail our max search size + + if file_service_key == CC.COMBINED_FILE_SERVICE_KEY: + + total_search_tag_count_threshold /= 25 + + + for ( search_tag_id, count ) in search_tag_ids_flat_sorted_ascending: + + # we don't want the total domain to be too large either. death by a thousand cuts + if total_count + count > total_search_tag_count_threshold: + + break + + + total_count += count + + search_tags.add( search_tag_ids_to_search_tags[ search_tag_id ] ) + + + if len( search_tags ) == 0: + + return [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = 'search domain too big' ) ] + + + # + + search_tag_ids_to_tag_ids_to_matching_counts = {} + + for search_tag in search_tags: + + search_tag_id = self.modules_tags_local_cache.GetTagId( search_tag ) + + tag_ids_to_matching_counts = self._GetRelatedTagsNewOneTag( tag_display_type, file_service_id, tag_service_id, search_tag_id ) + + if search_tag_id in tag_ids_to_matching_counts: + + del tag_ids_to_matching_counts[ search_tag_id ] # duh, don't recommend your 100% matching self + + + search_tag_ids_to_tag_ids_to_matching_counts[ search_tag_id ] = tag_ids_to_matching_counts + + + # + + # ok we have a bunch of counts here for different search tags, so let's figure out some normalised scores and merge them all + # + # the master score is: number matching mappings found / square_root( suggestion_tag_count * search_tag_count ) + # + # I don't really know what this *is*, but the user did it and it seems to make nice scores, so hooray + # the dude said it was arbitrary and could do with tuning, so we'll see how it goes + + all_tag_ids = set() + + for tag_ids_to_matching_counts in search_tag_ids_to_tag_ids_to_matching_counts.values(): + + all_tag_ids.update( tag_ids_to_matching_counts.keys() ) + + + all_tag_ids.difference_update( search_tag_ids_to_search_tags.keys() ) + + with self._MakeTemporaryIntegerTable( all_tag_ids, 'tag_id' ) as temp_tag_id_table_name: + + tag_ids_to_total_counts = { tag_id : current_count + pending_count for ( tag_id, current_count, pending_count ) in self.modules_mappings_counts.GetCountsForTags( tag_display_type, file_service_id, tag_service_id, temp_tag_id_table_name ) } + + + tag_ids_to_total_counts.update( search_tag_ids_to_total_counts ) + + tag_ids_to_scores = collections.Counter() + + for ( search_tag_id, tag_ids_to_matching_counts ) in search_tag_ids_to_tag_ids_to_matching_counts.items(): + + if search_tag_id not in tag_ids_to_total_counts: + + continue + + + search_tag_count = tag_ids_to_total_counts[ search_tag_id ] + + search_tag_is_unnamespaced = HydrusTags.IsUnnamespaced( search_tag_ids_to_search_tags[ search_tag_id ] ) + + for ( tag_id, matching_count ) in tag_ids_to_matching_counts.items(): + + if matching_count / search_tag_count < concurrence_threshold: + + continue + + + if tag_id not in tag_ids_to_total_counts: + + continue + + + suggestion_tag_count = tag_ids_to_total_counts[ tag_id ] + + score = matching_count / ( ( suggestion_tag_count * search_tag_count ) ** 0.5 ) + + # sophisticated hydev score-tuning + if search_tag_is_unnamespaced: + + score /= 3 + + + tag_ids_to_scores[ tag_id ] += score + + + + results_flat_sorted_descending = sorted( tag_ids_to_scores.items(), key = lambda row: row[1], reverse = True ) + + tag_ids_to_scores = dict( results_flat_sorted_descending[ : max_results ] ) + + # + + inclusive = True + pending_count = 0 + + tag_ids_to_full_counts = { tag_id : ( int( score * 1000 ), None, pending_count, None ) for ( tag_id, score ) in tag_ids_to_scores.items() } + + predicates = self.modules_tag_display.GeneratePredicatesFromTagIdsAndCounts( tag_display_type, tag_service_id, tag_ids_to_full_counts, inclusive ) + + return predicates + + def _GetRepositoryThumbnailHashesIDoNotHave( self, service_key ): service_id = self.modules_services.GetServiceId( service_key ) @@ -7379,6 +7619,7 @@ class DB( HydrusDB.HydrusDB ): elif action == 'services': result = self.modules_services.GetServices( *args, **kwargs ) elif action == 'similar_files_maintenance_status': result = self.modules_similar_files.GetMaintenanceStatus( *args, **kwargs ) elif action == 'related_tags': result = self._GetRelatedTags( *args, **kwargs ) + elif action == 'related_tags_new': result = self._GetRelatedTagsNew( *args, **kwargs ) elif action == 'tag_display_application': result = self.modules_tag_display.GetApplication( *args, **kwargs ) elif action == 'tag_display_maintenance_status': result = self._CacheTagDisplayGetApplicationStatusNumbers( *args, **kwargs ) elif action == 'tag_parents': result = self.modules_tag_parents.GetTagParents( *args, **kwargs ) @@ -10762,6 +11003,116 @@ class DB( HydrusDB.HydrusDB ): + if version == 513: + + try: + + self._controller.frame_splash_status.SetSubtext( 'cleaning some surplus records' ) + + # clear deletion record wasn't purging on 'all my files' + + all_my_deleted_hash_ids = set( self.modules_files_storage.GetDeletedHashIdsList( self.modules_services.combined_local_media_service_id ) ) + + all_local_current_hash_ids = self.modules_files_storage.GetCurrentHashIdsList( self.modules_services.combined_local_file_service_id ) + all_local_deleted_hash_ids = self.modules_files_storage.GetDeletedHashIdsList( self.modules_services.combined_local_file_service_id ) + + erroneous_hash_ids = all_my_deleted_hash_ids.difference( all_local_current_hash_ids ).difference( all_local_deleted_hash_ids ) + + if len( erroneous_hash_ids ) > 0: + + service_ids_to_nums_cleared = self.modules_files_storage.ClearLocalDeleteRecord( erroneous_hash_ids ) + + self._ExecuteMany( 'UPDATE service_info SET info = info + ? WHERE service_id = ? AND info_type = ?;', ( ( -num_cleared, clear_service_id, HC.SERVICE_INFO_NUM_DELETED_FILES ) for ( clear_service_id, num_cleared ) in service_ids_to_nums_cleared.items() ) ) + + + except: + + HydrusData.PrintException( e ) + + message = 'Trying to clean up some bad delete records failed! Please let hydrus dev know!' + + self.pub_initial_message( message ) + + + try: + + def ask_what_to_do_twitter_stuff(): + + message = 'Twitter removed their old API that we were using, breaking all the old downloaders! I am going to delete your old twitter downloaders and add a new limited one that can only get the first ~20 tweets of a profile. Make sure to check your subscriptions are linked to it, and you might want to speed up their check times! OK?' + + from hydrus.client.gui import ClientGUIDialogsQuick + + result = ClientGUIDialogsQuick.GetYesNo( None, message, title = 'Swap to new twitter downloader?', yes_label = 'do it', no_label = 'do not do it, I need to keep the old broken stuff' ) + + return result == QW.QDialog.Accepted + + + do_twitter_stuff = self._controller.CallBlockingToQt( None, ask_what_to_do_twitter_stuff ) + + domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER ) + + domain_manager.Initialise() + + # + + domain_manager.OverwriteDefaultParsers( [ + 'danbooru file page parser - get webm ugoira', + 'deviant art file page parser', + 'pixiv file page api parser' + ] ) + + if do_twitter_stuff: + + domain_manager.DeleteGUGs( [ + 'twitter collection lookup', + 'twitter likes lookup', + 'twitter list lookup' + ] ) + + url_classes = domain_manager.GetURLClasses() + + deletee_url_class_names = [ url_class.GetName() for url_class in url_classes if url_class.GetName() == 'twitter list' or url_class.GetName().startswith( 'twitter syndication api' ) ] + + domain_manager.DeleteURLClasses( deletee_url_class_names ) + + # we're going to leave the one spare non-overwritten parser in place + + # + + domain_manager.OverwriteDefaultGUGs( [ + 'twitter profile lookup', + 'twitter profile lookup (with replies)' + ] ) + + domain_manager.OverwriteDefaultURLClasses( [ + 'twitter tweet (i/web/status)', + 'twitter tweet', + 'twitter syndication api tweet-result', + 'twitter syndication api timeline-profile' + ] ) + + domain_manager.OverwriteDefaultParsers( [ + 'twitter syndication api timeline-profile parser', + 'twitter syndication api tweet parser' + ] ) + + + domain_manager.TryToLinkURLClassesAndParsers() + + # + + self.modules_serialisable.SetJSONDump( domain_manager ) + + except Exception as e: + + HydrusData.PrintException( e ) + + message = 'Trying to update some downloader objects failed! Please let hydrus dev know!' + + self.pub_initial_message( message ) + + + self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) ) self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) ) diff --git a/hydrus/client/db/ClientDBFilesStorage.py b/hydrus/client/db/ClientDBFilesStorage.py index b4384f7d..d96a51cd 100644 --- a/hydrus/client/db/ClientDBFilesStorage.py +++ b/hydrus/client/db/ClientDBFilesStorage.py @@ -373,7 +373,7 @@ class ClientDBFilesStorage( ClientDBModule.ClientDBModule ): service_ids_to_nums_cleared = {} - local_non_trash_service_ids = self.modules_services.GetServiceIds( ( HC.COMBINED_LOCAL_FILE, HC.LOCAL_FILE_DOMAIN ) ) + local_non_trash_service_ids = self.modules_services.GetServiceIds( ( HC.COMBINED_LOCAL_FILE, HC.COMBINED_LOCAL_MEDIA, HC.LOCAL_FILE_DOMAIN ) ) if hash_ids is None: diff --git a/hydrus/client/gui/ClientGUIGallerySeedLog.py b/hydrus/client/gui/ClientGUIGallerySeedLog.py index 21f61874..61a9af0e 100644 --- a/hydrus/client/gui/ClientGUIGallerySeedLog.py +++ b/hydrus/client/gui/ClientGUIGallerySeedLog.py @@ -447,17 +447,14 @@ class EditGallerySeedLogPanel( ClientGUIScrolledPanels.EditPanel ): def _TrySelectedAgain( self, can_generate_more_pages ): - new_gallery_seeds = [] - gallery_seeds = self._list_ctrl.GetData( only_selected = True ) for gallery_seed in gallery_seeds: - new_gallery_seeds.append( gallery_seed.GenerateRestartedDuplicate( can_generate_more_pages ) ) + restarted_gallery_seed = gallery_seed.GenerateRestartedDuplicate( can_generate_more_pages ) + + self._gallery_seed_log.AddGallerySeeds( ( restarted_gallery_seed, ), parent_gallery_seed = gallery_seed ) - - self._gallery_seed_log.AddGallerySeeds( new_gallery_seeds ) - self._gallery_seed_log.NotifyGallerySeedsUpdated( new_gallery_seeds ) def _UpdateListCtrl( self, gallery_seeds ): diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py index cf46003e..f895704a 100644 --- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py +++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py @@ -942,6 +942,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): self._delete_to_recycle_bin = QW.QCheckBox( self ) + self._ms_to_wait_between_physical_file_deletes = ClientGUICommon.BetterSpinBox( self, min=20, max = 5000 ) + tt = 'Deleting a file from a hard disk can be resource expensive, so when files leave the trash, the actual physical file delete happens later, in the background. The operation is spread out so as not to give you lag spikes.' + self._ms_to_wait_between_physical_file_deletes.setToolTip( tt ) + self._confirm_trash = QW.QCheckBox( self ) self._confirm_archive = QW.QCheckBox( self ) @@ -987,6 +991,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): self._delete_to_recycle_bin.setChecked( HC.options[ 'delete_to_recycle_bin' ] ) + self._ms_to_wait_between_physical_file_deletes.setValue( self._new_options.GetInteger( 'ms_to_wait_between_physical_file_deletes' ) ) + self._confirm_trash.setChecked( HC.options[ 'confirm_trash' ] ) self._confirm_archive.setChecked( HC.options[ 'confirm_archive' ] ) @@ -1028,7 +1034,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): rows.append( ( 'Confirm sending more than one file to archive or inbox: ', self._confirm_archive ) ) rows.append( ( 'Confirm when copying files across local file services: ', self._confirm_multiple_local_file_services_copy ) ) rows.append( ( 'Confirm when moving files across local file services: ', self._confirm_multiple_local_file_services_move ) ) - rows.append( ( 'When deleting files or folders, send them to the OS\'s recycle bin: ', self._delete_to_recycle_bin ) ) + rows.append( ( 'When physically deleting files or folders, send them to the OS\'s recycle bin: ', self._delete_to_recycle_bin ) ) + rows.append( ( 'When maintenance physically deletes files, wait this many ms between each delete: ', self._ms_to_wait_between_physical_file_deletes ) ) rows.append( ( 'Remove files from view when they are filtered: ', self._remove_filtered_files ) ) rows.append( ( 'Remove files from view when they are sent to the trash: ', self._remove_trashed_files ) ) rows.append( ( 'Number of hours a file can be in the trash before being deleted: ', self._trash_max_age ) ) @@ -1120,6 +1127,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): HC.options[ 'trash_max_age' ] = self._trash_max_age.GetValue() HC.options[ 'trash_max_size' ] = self._trash_max_size.GetValue() + self._new_options.SetInteger( 'ms_to_wait_between_physical_file_deletes', self._ms_to_wait_between_physical_file_deletes.value() ) + self._new_options.SetBoolean( 'confirm_multiple_local_file_services_copy', self._confirm_multiple_local_file_services_copy.isChecked() ) self._new_options.SetBoolean( 'confirm_multiple_local_file_services_move', self._confirm_multiple_local_file_services_move.isChecked() ) @@ -3861,14 +3870,14 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): rows = [] - rows.append( ( 'Show related tags on single-file manage tags windows: ', self._show_related_tags ) ) + rows.append( ( 'Show related tags: ', self._show_related_tags ) ) rows.append( ( 'Initial search duration (ms): ', self._related_tags_search_1_duration_ms ) ) rows.append( ( 'Medium search duration (ms): ', self._related_tags_search_2_duration_ms ) ) rows.append( ( 'Thorough search duration (ms): ', self._related_tags_search_3_duration_ms ) ) gridbox = ClientGUICommon.WrapInGrid( suggested_tags_related_panel, rows ) - desc = 'This will search the database for statistically related tags based on what your focused file already has.' + desc = 'This will search the database for tags statistically related to what your files already have.' QP.AddToLayout( panel_vbox, ClientGUICommon.BetterStaticText(suggested_tags_related_panel,desc), CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( panel_vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py index 0b93497f..0f6a4630 100644 --- a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py +++ b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py @@ -2450,15 +2450,6 @@ class ReviewFileHistory( ClientGUIScrolledPanels.ReviewPanel ): vbox = QP.VBoxLayout() - label = 'Please note that delete and inbox time tracking are new so you may not have full data for them.' - - st = ClientGUICommon.BetterStaticText( self, label = label ) - - st.setWordWrap( True ) - st.setAlignment( QC.Qt.AlignCenter ) - - QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR ) - flip_deleted = QW.QCheckBox( 'show deleted', self ) flip_deleted.setChecked( True ) @@ -3424,6 +3415,8 @@ class ReviewLocalFileImports( ClientGUIScrolledPanels.ReviewPanel ): num_unimportable_mime_files = 0 num_occupied_files = 0 + num_sidecars = 0 + while not HG.started_shutdown: if not self or not QP.isValid( self ): @@ -3462,7 +3455,7 @@ class ReviewLocalFileImports( ClientGUIScrolledPanels.ReviewPanel ): message = HydrusData.ConvertValueRangeToPrettyString( num_good_files, total_paths ) + ' files parsed successfully' - if num_empty_files + num_missing_files + num_unimportable_mime_files + num_occupied_files > 0: + if num_empty_files + num_missing_files + num_unimportable_mime_files + num_occupied_files + num_sidecars > 0: if num_good_files == 0: @@ -3475,6 +3468,11 @@ class ReviewLocalFileImports( ClientGUIScrolledPanels.ReviewPanel ): bad_comments = [] + if num_sidecars > 0: + + bad_comments.append( '{} looked like txt/json/xml sidecars'.format( HydrusData.ToHumanInt( num_sidecars ) ) ) + + if num_empty_files > 0: bad_comments.append( HydrusData.ToHumanInt( num_empty_files ) + ' were empty' ) @@ -3544,9 +3542,10 @@ class ReviewLocalFileImports( ClientGUIScrolledPanels.ReviewPanel ): try: raw_paths = unparsed_paths_queue.get( block = False ) + + ( paths, num_sidecars_this_loop ) = ClientFiles.GetAllFilePaths( raw_paths, do_human_sort = do_human_sort ) # convert any dirs to subpaths - paths = ClientFiles.GetAllFilePaths( raw_paths, do_human_sort = do_human_sort ) # convert any dirs to subpaths - + num_sidecars += num_sidecars_this_loop unparsed_paths.extend( paths ) except queue.Empty: @@ -3562,9 +3561,10 @@ class ReviewLocalFileImports( ClientGUIScrolledPanels.ReviewPanel ): try: raw_paths = unparsed_paths_queue.get( timeout = 5 ) + + ( paths, num_sidecars_this_loop ) = ClientFiles.GetAllFilePaths( raw_paths, do_human_sort = do_human_sort ) # convert any dirs to subpaths - paths = ClientFiles.GetAllFilePaths( raw_paths, do_human_sort = do_human_sort ) # convert any dirs to subpaths - + num_sidecars += num_sidecars_this_loop unparsed_paths.extend( paths ) except queue.Empty: diff --git a/hydrus/client/gui/ClientGUISubscriptions.py b/hydrus/client/gui/ClientGUISubscriptions.py index bf32f538..c63aab8e 100644 --- a/hydrus/client/gui/ClientGUISubscriptions.py +++ b/hydrus/client/gui/ClientGUISubscriptions.py @@ -189,7 +189,7 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ): if HG.client_controller.new_options.GetBoolean( 'advanced_mode' ): - limits_max = 10000 + limits_max = 50000 else: diff --git a/hydrus/client/gui/ClientGUITagSuggestions.py b/hydrus/client/gui/ClientGUITagSuggestions.py index ed930361..3d61e85f 100644 --- a/hydrus/client/gui/ClientGUITagSuggestions.py +++ b/hydrus/client/gui/ClientGUITagSuggestions.py @@ -1,3 +1,4 @@ +import os import typing from qtpy import QtCore as QC @@ -35,14 +36,18 @@ def FilterSuggestedPredicatesForMedia( predicates: typing.Sequence[ ClientSearch def FilterSuggestedTagsForMedia( tags: typing.Sequence[ str ], medias: typing.Collection[ ClientMedia.Media ], service_key: bytes ) -> typing.List[ str ]: + # TODO: figure out a nice way to filter out siblings here + # maybe have to wait for when tags always know their siblings + # then we could also filter out worse/better siblings of the same count + + num_media = len( medias ) + tags_filtered_set = set( tags ) ( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientMedia.GetMediasTagCount( medias, service_key, ClientTags.TAG_DISPLAY_STORAGE ) current_tags_to_count.update( pending_tags_to_count ) - num_media = len( medias ) - for ( tag, count ) in current_tags_to_count.items(): if count == num_media: @@ -331,17 +336,46 @@ class RelatedTagsPanel( QW.QWidget ): self._have_fetched = False + self._selected_tags = set() + self._new_options = HG.client_controller.new_options vbox = QP.VBoxLayout() + tt = 'If you select some tags, this will search using only those as reference!' + self._button_2 = QW.QPushButton( 'medium', self ) self._button_2.clicked.connect( self.RefreshMedium ) self._button_2.setMinimumWidth( 30 ) + self._button_2.setToolTip( tt ) self._button_3 = QW.QPushButton( 'thorough', self ) self._button_3.clicked.connect( self.RefreshThorough ) self._button_3.setMinimumWidth( 30 ) + self._button_3.setToolTip( tt ) + + self._button_new = QW.QPushButton( 'new 1', self ) + self._button_new.clicked.connect( self.RefreshNew ) + self._button_new.setMinimumWidth( 30 ) + tt = 'Please test this! This uses the new statistical method and searches your local files\' tags. Should be pretty fast, but its search domain is limited.' + os.linesep * 2 + 'Hydev thinks this mode sucks for the PTR, so let him know if it is actually works ok there.' + self._button_new.setToolTip( tt ) + + self._button_new_2 = QW.QPushButton( 'new 2', self ) + self._button_new_2.clicked.connect( self.RefreshNew2 ) + self._button_new_2.setMinimumWidth( 30 ) + tt = 'Please test this! This uses the new statistical method and searches all the service\'s tags. May search slow and will not get results from large-count tags.' + os.linesep * 2 + 'Hydev wants to use this in the end, so let him know if it is too laggy.' + self._button_new_2.setToolTip( tt ) + + if len( self._media ) > 1: + + self._button_2.setVisible( False ) + self._button_3.setVisible( False ) + + + if HG.client_controller.services_manager.GetServiceType( self._service_key ) == HC.LOCAL_TAG: + + self._button_new_2.setVisible( False ) + self._related_tags = ListBoxTagsSuggestionsRelated( self, service_key, activate_callable ) @@ -349,6 +383,8 @@ class RelatedTagsPanel( QW.QWidget ): QP.AddToLayout( button_hbox, self._button_2, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) QP.AddToLayout( button_hbox, self._button_3, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) + QP.AddToLayout( button_hbox, self._button_new, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) + QP.AddToLayout( button_hbox, self._button_new_2, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) QP.AddToLayout( vbox, button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._related_tags, CC.FLAGS_EXPAND_BOTH_WAYS ) @@ -385,17 +421,77 @@ class RelatedTagsPanel( QW.QWidget ): self._related_tags.SetPredicates( [] ) + if len( self._media ) > 1: + + return + + ( m, ) = self._media hash = m.GetHash() - search_tags = ClientMedia.GetMediasTags( self._media, self._service_key, ClientTags.TAG_DISPLAY_STORAGE, ( HC.CONTENT_STATUS_CURRENT, HC.CONTENT_STATUS_PENDING ) ) + # TODO: If user has some tags selected, use them instead + + if len( self._selected_tags ) == 0: + + search_tags = ClientMedia.GetMediasTags( self._media, self._service_key, ClientTags.TAG_DISPLAY_STORAGE, ( HC.CONTENT_STATUS_CURRENT, HC.CONTENT_STATUS_PENDING ) ) + + else: + + search_tags = self._selected_tags + max_results = 100 HG.client_controller.CallToThread( do_it, self._service_key, hash, search_tags, max_results, max_time_to_take ) + def _FetchRelatedTagsNew( self, file_service_key = None ): + + def do_it( file_service_key, tag_service_key, search_tags ): + + def qt_code( predicates ): + + if not self or not QP.isValid( self ): + + return + + + self._last_fetched_predicates = predicates + + self._UpdateTagDisplay() + + self._have_fetched = True + + + predicates = HG.client_controller.Read( 'related_tags_new', file_service_key, tag_service_key, search_tags ) + + predicates = ClientSearch.SortPredicates( predicates ) + + QP.CallAfter( qt_code, predicates ) + + + self._related_tags.SetPredicates( [] ) + + if len( self._selected_tags ) == 0: + + search_tags = ClientMedia.GetMediasTags( self._media, self._service_key, ClientTags.TAG_DISPLAY_STORAGE, ( HC.CONTENT_STATUS_CURRENT, HC.CONTENT_STATUS_PENDING ) ) + + else: + + search_tags = self._selected_tags + + + if file_service_key is None: + + file_service_key = CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY + + + tag_service_key = self._service_key + + HG.client_controller.CallToThread( do_it, file_service_key, tag_service_key, search_tags ) + + def _QuickSuggestedRelatedTags( self ): max_time_to_take = self._new_options.GetInteger( 'related_tags_search_1_duration_ms' ) / 1000.0 @@ -424,6 +520,16 @@ class RelatedTagsPanel( QW.QWidget ): self._FetchRelatedTags( max_time_to_take ) + def RefreshNew( self ): + + self._FetchRelatedTagsNew() + + + def RefreshNew2( self ): + + self._FetchRelatedTagsNew( file_service_key = CC.COMBINED_FILE_SERVICE_KEY ) + + def MediaUpdated( self ): self._UpdateTagDisplay() @@ -433,7 +539,19 @@ class RelatedTagsPanel( QW.QWidget ): self._media = media - self._QuickSuggestedRelatedTags() + if len( self._media ) == 1: + + self._QuickSuggestedRelatedTags() + + else: + + self._related_tags.SetPredicates( [] ) + + + + def SetSelectedTags( self, tags ): + + self._selected_tags = tags def TakeFocusForUser( self ): @@ -682,7 +800,7 @@ class SuggestedTagsPanel( QW.QWidget ): self._related_tags = None - if self._new_options.GetBoolean( 'show_related_tags' ) and len( media ) == 1: + if self._new_options.GetBoolean( 'show_related_tags' ): self._related_tags = RelatedTagsPanel( panel_parent, service_key, media, activate_callable ) @@ -796,6 +914,14 @@ class SuggestedTagsPanel( QW.QWidget ): + def SetSelectedTags( self, tags ): + + if self._related_tags is not None: + + self._related_tags.SetSelectedTags( tags ) + + + def TakeFocusForUser( self, command ): if command == CAC.SIMPLE_SHOW_AND_FOCUS_MANAGE_TAGS_FAVOURITE_TAGS: diff --git a/hydrus/client/gui/ClientGUITags.py b/hydrus/client/gui/ClientGUITags.py index 4a383aea..5747d981 100644 --- a/hydrus/client/gui/ClientGUITags.py +++ b/hydrus/client/gui/ClientGUITags.py @@ -2298,6 +2298,8 @@ class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel, CAC.ApplicationComma self._suggested_tags.mouseActivationOccurred.connect( self.SetTagBoxFocus ) + self._tags_box.tagsSelected.connect( self._suggested_tags.SetSelectedTags ) + def _EnterTags( self, tags, only_add = False, only_remove = False, forced_reason = None ): @@ -4473,18 +4475,33 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ): current_dict = dict( current_pairs ) - current_olds = set( current_dict.keys() ) - for ( potential_old, potential_new ) in pairs: if potential_new in current_dict: loop_new = potential_new + seen_tags = set() + while loop_new in current_dict: + seen_tags.add( loop_new ) + next_new = current_dict[ loop_new ] + if next_new in seen_tags: + + QW.QMessageBox.warning( self, 'Loop Problem!', 'While trying to test the new pair(s) for potential loops, I think I ran across an existing loop! Please review everything and see if you can break any loops yourself.' ) + + message = 'The pair you mean to add seems to connect to a sibling loop already in your database! Please undo this loop manually. The tags involved in the loop are:' + message += os.linesep * 2 + message += ', '.join( seen_tags ) + + QW.QMessageBox.critical( self, 'Error', message ) + + break + + if next_new == potential_old: pairs_to_auto_petition = [ ( loop_new, next_new ) ] diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py index 7702c171..892eca3c 100644 --- a/hydrus/client/gui/canvas/ClientGUICanvas.py +++ b/hydrus/client/gui/canvas/ClientGUICanvas.py @@ -2098,7 +2098,7 @@ class CanvasWithHovers( CanvasWithDetails ): # due to the mouse setPos below, the event pos can get funky I think due to out of order coordinate setting events, so we'll poll current value directly event_pos = self.mapFromGlobal( QG.QCursor.pos() ) - mouse_currently_shown = self.cursor() == QG.QCursor( QC.Qt.ArrowCursor ) + mouse_currently_shown = self.cursor().shape() == QC.Qt.ArrowCursor show_mouse = mouse_currently_shown is_dragging = event.buttons() & QC.Qt.LeftButton and self._last_drag_pos is not None diff --git a/hydrus/client/gui/exporting/ClientGUIExport.py b/hydrus/client/gui/exporting/ClientGUIExport.py index c400118a..40734319 100644 --- a/hydrus/client/gui/exporting/ClientGUIExport.py +++ b/hydrus/client/gui/exporting/ClientGUIExport.py @@ -83,6 +83,27 @@ class EditExportFoldersPanel( ClientGUIScrolledPanels.EditPanel ): metadata_routers = new_options.GetDefaultExportFilesMetadataRouters() + if len( metadata_routers ) > 0: + + message = 'You have some default metadata sidecar settings, most likely from a previous file export. They look like this:' + message += os.linesep * 2 + message += os.linesep.join( [ router.ToString( pretty = True ) for router in metadata_routers ] ) + message += os.linesep * 2 + message += 'Do you want these in the new export folder?' + + ( result, cancelled ) = ClientGUIDialogsQuick.GetYesNo( self, message, no_label = 'no, I want an empty sidecar list', check_for_cancelled = True ) + + if cancelled: + + return + + + if result != QW.QDialog.DialogCode.Accepted: + + metadata_routers = [] + + + period = 15 * 60 export_folder = ClientExportingFiles.ExportFolder( diff --git a/hydrus/client/gui/lists/ClientGUIListBoxes.py b/hydrus/client/gui/lists/ClientGUIListBoxes.py index 747b7e64..009672f0 100644 --- a/hydrus/client/gui/lists/ClientGUIListBoxes.py +++ b/hydrus/client/gui/lists/ClientGUIListBoxes.py @@ -1489,6 +1489,8 @@ class ListBox( QW.QScrollArea ): + self._SelectionChanged() + self.widget().update() @@ -1781,6 +1783,11 @@ class ListBox( QW.QScrollArea ): self._selected_terms = set( self._terms_to_logical_indices.keys() ) + def _SelectionChanged( self ): + + pass + + def _SetVirtualSize( self ): self.setWidgetResizable( True ) @@ -2169,6 +2176,7 @@ COPY_ALL_SUBTAGS_WITH_COUNTS = 7 class ListBoxTags( ListBox ): + tagsSelected = QC.Signal( set ) can_spawn_new_windows = True def __init__( self, parent, *args, tag_display_type: int = ClientTags.TAG_DISPLAY_STORAGE, **kwargs ): @@ -2437,6 +2445,13 @@ class ListBoxTags( ListBox ): pass + def _SelectionChanged( self ): + + tags = set( self._GetTagsFromTerms( self._selected_terms ) ) + + self.tagsSelected.emit( tags ) + + def _UpdateBackgroundColour( self ): new_options = HG.client_controller.new_options diff --git a/hydrus/client/gui/pages/ClientGUIManagement.py b/hydrus/client/gui/pages/ClientGUIManagement.py index 861e3739..9f16a001 100644 --- a/hydrus/client/gui/pages/ClientGUIManagement.py +++ b/hydrus/client/gui/pages/ClientGUIManagement.py @@ -996,6 +996,8 @@ class ManagementPanel( QW.QScrollArea ): self._page = page self._page_key = self._management_controller.GetVariable( 'page_key' ) + self._page_state = CC.PAGE_STATE_NORMAL + self._current_selection_tags_list = None self._media_sort = ClientGUIResultsSortCollect.MediaSortControl( self, management_controller = self._management_controller ) @@ -1097,6 +1099,11 @@ class ManagementPanel( QW.QScrollArea ): return media_panel + def GetPageState( self ) -> int: + + return self._page_state + + def PageHidden( self ): pass @@ -1620,16 +1627,20 @@ class ManagementPanelDuplicateFilter( ManagementPanel ): self._page.SwapMediaPanel( panel ) + self._page_state = CC.PAGE_STATE_NORMAL + def _ShowRandomPotentialDupes( self ): ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData() + self._page_state = CC.PAGE_STATE_SEARCHING + hashes = self._controller.Read( 'random_potential_duplicate_hashes', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) if len( hashes ) == 0: - HydrusData.ShowText( 'No files were found. Try refreshing the count, and if this keeps happening, please let hydrus_dev know.' ) + HydrusData.ShowText( 'No random potential duplicates were found. Try refreshing the count, and if this keeps happening, please let hydrus_dev know.' ) self._ShowPotentialDupes( hashes ) @@ -5501,6 +5512,8 @@ class ManagementPanelQuery( ManagementPanel ): self._page.SwapMediaPanel( panel ) + self._page_state = CC.PAGE_STATE_SEARCHING_CANCELLED + self._UpdateCancelButton() @@ -5576,6 +5589,8 @@ class ManagementPanelQuery( ManagementPanel ): panel = ClientGUIResults.MediaPanelLoading( self._page, self._page_key, location_context ) + self._page_state = CC.PAGE_STATE_SEARCHING + else: panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, location_context, [] ) @@ -5749,6 +5764,8 @@ class ManagementPanelQuery( ManagementPanel ): self._page.SwapMediaPanel( panel ) + self._page_state = CC.PAGE_STATE_NORMAL + def Start( self ): diff --git a/hydrus/client/gui/pages/ClientGUIPages.py b/hydrus/client/gui/pages/ClientGUIPages.py index c26ff0c3..ac861f7d 100644 --- a/hydrus/client/gui/pages/ClientGUIPages.py +++ b/hydrus/client/gui/pages/ClientGUIPages.py @@ -664,6 +664,7 @@ class Page( QW.QWidget ): d[ 'name' ] = self._management_controller.GetPageName() d[ 'page_key' ] = self._page_key.hex() + d[ 'page_state' ] = self.GetPageState() d[ 'page_type' ] = self._management_controller.GetType() management_info = self._management_controller.GetAPIInfoDict( simple ) @@ -770,6 +771,18 @@ class Page( QW.QWidget ): return { self._page_key } + def GetPageState( self ) -> int: + + if self._initialised: + + return self._management_panel.GetPageState() + + else: + + return CC.PAGE_STATE_INITIALISING + + + def GetParentNotebook( self ): return self._parent_notebook @@ -819,6 +832,7 @@ class Page( QW.QWidget ): root[ 'name' ] = self.GetName() root[ 'page_key' ] = self._page_key.hex() + root[ 'page_state' ] = self.GetPageState() root[ 'page_type' ] = self._management_controller.GetType() root[ 'selected' ] = is_selected @@ -2286,7 +2300,12 @@ class PagesNotebook( QP.TabWidgetWithDnD ): def GetAPIInfoDict( self, simple ): - return {} + return { + 'name' : self.GetName(), + 'page_key' : self._page_key.hex(), + 'page_state' : self.GetPageState(), + 'page_type' : ClientGUIManagement.MANAGEMENT_TYPE_PAGE_OF_PAGES + } def GetCurrentGUISession( self, name: str, only_changed_page_data: bool, about_to_save: bool ): @@ -2540,6 +2559,11 @@ class PagesNotebook( QP.TabWidgetWithDnD ): return self._GetPages() + def GetPageState( self ) -> int: + + return CC.PAGE_STATE_NORMAL + + def GetPrettyStatusForStatusBar( self ): ( num_files, ( num_value, num_range ) ) = self.GetNumFileSummary() @@ -2596,6 +2620,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ): root[ 'name' ] = self.GetName() root[ 'page_key' ] = self._page_key.hex() + root[ 'page_state' ] = self.GetPageState() root[ 'page_type' ] = ClientGUIManagement.MANAGEMENT_TYPE_PAGE_OF_PAGES root[ 'selected' ] = is_selected root[ 'pages' ] = my_pages_list diff --git a/hydrus/client/gui/search/ClientGUIACDropdown.py b/hydrus/client/gui/search/ClientGUIACDropdown.py index 7aea7d0e..36ce4cea 100644 --- a/hydrus/client/gui/search/ClientGUIACDropdown.py +++ b/hydrus/client/gui/search/ClientGUIACDropdown.py @@ -43,7 +43,9 @@ def InsertOtherPredicatesForRead( predicates: list, parsed_autocomplete_text: Cl if include_unusual_predicate_types: - non_tag_predicates = list( parsed_autocomplete_text.GetNonTagFileSearchPredicates() ) + allow_auto_wildcard_conversion = True + + non_tag_predicates = list( parsed_autocomplete_text.GetNonTagFileSearchPredicates( allow_auto_wildcard_conversion ) ) non_tag_predicates.reverse() @@ -58,11 +60,11 @@ def InsertOtherPredicatesForRead( predicates: list, parsed_autocomplete_text: Cl PutAtTopOfMatches( predicates, under_construction_or_predicate ) -def InsertTagPredicates( predicates: list, tag_service_key: bytes, parsed_autocomplete_text: ClientSearch.ParsedAutocompleteText, insert_if_does_not_exist: bool = True ): +def InsertTagPredicates( predicates: list, tag_service_key: bytes, parsed_autocomplete_text: ClientSearch.ParsedAutocompleteText, allow_auto_wildcard_conversion: bool, insert_if_does_not_exist: bool = True ): - if parsed_autocomplete_text.IsTagSearch(): + if parsed_autocomplete_text.IsTagSearch( allow_auto_wildcard_conversion): - tag_predicate = parsed_autocomplete_text.GetImmediateFileSearchPredicate() + tag_predicate = parsed_autocomplete_text.GetImmediateFileSearchPredicate( allow_auto_wildcard_conversion ) actual_tag = tag_predicate.GetValue() @@ -185,7 +187,9 @@ def ReadFetch( if fetch_from_db: - is_explicit_wildcard = parsed_autocomplete_text.IsExplicitWildcard() + allow_auto_wildcard_conversion = True + + is_explicit_wildcard = parsed_autocomplete_text.IsExplicitWildcard( allow_auto_wildcard_conversion ) small_exact_match_search = ShouldDoExactSearch( parsed_autocomplete_text ) @@ -340,7 +344,9 @@ def ReadFetch( - InsertTagPredicates( matches, tag_service_key, parsed_autocomplete_text, insert_if_does_not_exist = False ) + allow_auto_wildcard_conversion = True + + InsertTagPredicates( matches, tag_service_key, parsed_autocomplete_text, allow_auto_wildcard_conversion, insert_if_does_not_exist = False ) InsertOtherPredicatesForRead( matches, parsed_autocomplete_text, include_unusual_predicate_types, under_construction_or_predicate ) @@ -376,7 +382,9 @@ def PutAtTopOfMatches( matches: list, predicate: ClientSearch.Predicate, insert_ def ShouldDoExactSearch( parsed_autocomplete_text: ClientSearch.ParsedAutocompleteText ): - if parsed_autocomplete_text.IsExplicitWildcard(): + allow_auto_wildcard_conversion = True + + if parsed_autocomplete_text.IsExplicitWildcard( allow_auto_wildcard_conversion ): return False @@ -418,9 +426,12 @@ def WriteFetch( win, job_key, results_callable, parsed_autocomplete_text: Client else: - is_explicit_wildcard = parsed_autocomplete_text.IsExplicitWildcard() + allow_auto_wildcard_conversion = False - strict_search_text = parsed_autocomplete_text.GetSearchText( False ) + # TODO: This allow_auto_wildcard_conversion hack to handle allow_unnamespaced_search_gives_any_namespace_wildcards is hell. I should write IsImplicitWildcard or something! + is_explicit_wildcard = parsed_autocomplete_text.IsExplicitWildcard( allow_auto_wildcard_conversion ) + + strict_search_text = parsed_autocomplete_text.GetSearchText( False, allow_auto_wildcard_conversion = allow_auto_wildcard_conversion ) autocomplete_search_text = parsed_autocomplete_text.GetSearchText( True ) small_exact_match_search = ShouldDoExactSearch( parsed_autocomplete_text ) @@ -490,7 +501,9 @@ def WriteFetch( win, job_key, results_callable, parsed_autocomplete_text: Client matches = ClientSearch.SortPredicates( matches ) - InsertTagPredicates( matches, display_tag_service_key, parsed_autocomplete_text ) + allow_auto_wildcard_conversion = False + + InsertTagPredicates( matches, display_tag_service_key, parsed_autocomplete_text, allow_auto_wildcard_conversion ) HG.client_controller.CallAfterQtSafe( win, 'write a/c fetch', results_callable, job_key, parsed_autocomplete_text, results_cache, matches ) @@ -1909,7 +1922,9 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): if parsed_autocomplete_text.IsAcceptableForFileSearches(): - return parsed_autocomplete_text.GetImmediateFileSearchPredicate() + allow_auto_wildcard_conversion = True + + return parsed_autocomplete_text.GetImmediateFileSearchPredicate( allow_auto_wildcard_conversion ) else: @@ -2656,9 +2671,11 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ): parsed_autocomplete_text = self._GetParsedAutocompleteText() - if parsed_autocomplete_text.IsTagSearch(): + allow_auto_wildcard_conversion = False + + if parsed_autocomplete_text.IsTagSearch( allow_auto_wildcard_conversion ): - return parsed_autocomplete_text.GetImmediateFileSearchPredicate() + return parsed_autocomplete_text.GetImmediateFileSearchPredicate( allow_auto_wildcard_conversion ) else: @@ -2765,7 +2782,9 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ): stub_predicates = [] - InsertTagPredicates( stub_predicates, self._display_tag_service_key, parsed_autocomplete_text ) + allow_auto_wildcard_conversion = False + + InsertTagPredicates( stub_predicates, self._display_tag_service_key, parsed_autocomplete_text, allow_auto_wildcard_conversion ) AppendLoadingPredicate( stub_predicates ) diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py index a97d7b68..03a3ad8b 100644 --- a/hydrus/client/gui/services/ClientGUIClientsideServices.py +++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py @@ -1602,7 +1602,6 @@ class ReviewServicePanel( QW.QWidget ): if not HG.client_controller.new_options.GetBoolean( 'advanced_mode' ): self._id_button.hide() - self._service_key_button.hide() vbox = QP.VBoxLayout() diff --git a/hydrus/client/importing/ClientImportFileSeeds.py b/hydrus/client/importing/ClientImportFileSeeds.py index 0bf4d4ea..5818455a 100644 --- a/hydrus/client/importing/ClientImportFileSeeds.py +++ b/hydrus/client/importing/ClientImportFileSeeds.py @@ -1,4 +1,3 @@ -import bisect import collections import itertools import os @@ -1921,8 +1920,61 @@ class FileSeedCacheStatus( HydrusSerialisable.SerialisableBase ): self._latest_added_time = latest_added_time + HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_FILE_SEED_CACHE_STATUS ] = FileSeedCacheStatus +def WalkToNextFileSeed( status, starting_file_seed, file_seeds, file_seeds_to_indices, statuses_to_file_seeds ): + + # the file seed given is the starting point and can be the answer + # but if it is wrong, or no longer tracked, then let's walk through them until we get one, or None + # along the way, we'll note what's in the wrong place + + wrong_file_seeds = set() + + if starting_file_seed in file_seeds_to_indices: + + index = file_seeds_to_indices[ starting_file_seed ] + + else: + + index = 0 + + + file_seed = starting_file_seed + + while True: + + # no need to walk further, we are good + + if file_seed.status == status: + + result = file_seed + + break + + + # this file seed has the wrong status, move on + + if file_seed in statuses_to_file_seeds[ status ]: + + wrong_file_seeds.add( file_seed ) + + + index += 1 + + if index >= len( file_seeds ): + + result = None + + break + + + file_seed = file_seeds[ index ] + + + return ( result, wrong_file_seeds ) + + class FileSeedCache( HydrusSerialisable.SerialisableBase ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_FILE_SEED_CACHE @@ -1939,14 +1991,21 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): self._file_seeds_to_indices = {} - self._statuses_to_indexed_file_seeds = collections.defaultdict( list ) + # if there's a file seed here, it is the earliest + # if there's none in here, we know the count is 0 + # if status is absent, we don't know + self._observed_statuses_to_next_file_seeds = {} + + self._file_seeds_to_observed_statuses = {} + self._statuses_to_file_seeds = collections.defaultdict( set ) self._file_seed_cache_key = HydrusData.GenerateKey() self._status_cache = FileSeedCacheStatus() self._status_dirty = True - self._statuses_to_indexed_file_seeds_dirty = True + self._statuses_to_file_seeds_dirty = True + self._file_seeds_to_indices_dirty = True self._lock = threading.Lock() @@ -1956,56 +2015,88 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): return len( self._file_seeds ) - def _FileSeedIndicesJustChanged( self ): + def _FixStatusesToFileSeeds( self, file_seeds: typing.Collection[ FileSeed ] ): - self._file_seeds_to_indices = { file_seed : index for ( index, file_seed ) in enumerate( self._file_seeds ) } + if self._statuses_to_file_seeds_dirty: + + return + - self._SetStatusesToFileSeedsDirty() + file_seeds_to_indices = self._GetFileSeedsToIndices() + statuses_to_file_seeds = self._GetStatusesToFileSeeds() - - def _FixFileSeedsStatusPosition( self, file_seeds ): + if len( file_seeds ) == 0: + + return + - indices_and_file_seeds_affected = [] + outstanding_others_to_fix = set() for file_seed in file_seeds: - if file_seed in self._file_seeds_to_indices: - - indices_and_file_seeds_affected.append( ( self._file_seeds_to_indices[ file_seed ], file_seed ) ) - - else: - - self._SetStatusesToFileSeedsDirty() - - return - + want_a_record = file_seed in file_seeds_to_indices + record_exists = file_seed in self._file_seeds_to_observed_statuses - - for row in indices_and_file_seeds_affected: - - correct_status = row[1].status - - if row in self._statuses_to_indexed_file_seeds[ correct_status ]: + if not want_a_record and not record_exists: continue - for ( status, indices_and_file_seeds ) in self._statuses_to_indexed_file_seeds.items(): + correct_status = file_seed.status + + if record_exists: - if status == correct_status: + set_status = self._file_seeds_to_observed_statuses[ file_seed ] + + if set_status != correct_status or not want_a_record: - continue + if set_status in self._observed_statuses_to_next_file_seeds: + + if self._observed_statuses_to_next_file_seeds[ set_status ] == file_seed: + + # this 'next' is now wrong, so fast forward to the correct one, or None + ( result, wrong_file_seeds ) = WalkToNextFileSeed( set_status, file_seed, self._file_seeds, file_seeds_to_indices, statuses_to_file_seeds ) + + self._observed_statuses_to_next_file_seeds[ set_status ] = result + + outstanding_others_to_fix.update( wrong_file_seeds ) + + + + statuses_to_file_seeds[ set_status ].discard( file_seed ) + + del self._file_seeds_to_observed_statuses[ file_seed ] + + record_exists = False - if row in indices_and_file_seeds: + + if want_a_record: + + if not record_exists: - indices_and_file_seeds.remove( row ) + statuses_to_file_seeds[ correct_status ].add( file_seed ) - bisect.insort( self._statuses_to_indexed_file_seeds[ correct_status ], row ) - - break + self._file_seeds_to_observed_statuses[ file_seed ] = correct_status + if correct_status in self._observed_statuses_to_next_file_seeds: + + current_next_file_seed = self._observed_statuses_to_next_file_seeds[ correct_status ] + + if current_next_file_seed is None or file_seeds_to_indices[ file_seed ] < file_seeds_to_indices[ current_next_file_seed ]: + + self._observed_statuses_to_next_file_seeds[ correct_status ] = file_seed + + + + + + outstanding_others_to_fix.difference_update( file_seeds ) + + if len( outstanding_others_to_fix ) > 0: + + self._FixStatusesToFileSeeds( outstanding_others_to_fix ) @@ -2029,15 +2120,25 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): else: - if self._statuses_to_indexed_file_seeds_dirty: - - self._RegenerateStatusesToFileSeeds() - + statuses_to_file_seeds = self._GetStatusesToFileSeeds() + file_seeds_to_indices = self._GetFileSeedsToIndices() - return [ file_seed for ( index, file_seed ) in self._statuses_to_indexed_file_seeds[ status ] ] + return sorted( statuses_to_file_seeds[ status ], key = lambda f_s: file_seeds_to_indices[ f_s ] ) + def _GetFileSeedsToIndices( self ) -> typing.Dict[ FileSeed, int ]: + + if self._file_seeds_to_indices_dirty: + + self._file_seeds_to_indices = { file_seed : index for ( index, file_seed ) in enumerate( self._file_seeds ) } + + self._file_seeds_to_indices_dirty = False + + + return self._file_seeds_to_indices + + def _GetLatestAddedTime( self ): if len( self._file_seeds ) == 0: @@ -2069,36 +2170,41 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): def _GetNextFileSeed( self, status: int ) -> typing.Optional[ FileSeed ]: + statuses_to_file_seeds = self._GetStatusesToFileSeeds() + file_seeds_to_indices = self._GetFileSeedsToIndices() + # the problem with this is if a file seed recently changed but 'notifyupdated' hasn't had a chance to go yet # there could be a FS in a list other than the one we are looking at that has the status we want - # _however_, it seems like I do not do any async calls to notifyupdated in the actual FSC, only from notifyupdated to GUI elements, so we _seem_ to be good + # _however_, it seems like I do not do any async calls to notifyupdated in the actual FSC, only from notifyupdated to GUI elements, so we _seem_ to be good to talk to this in this way - if self._statuses_to_indexed_file_seeds_dirty: + if status not in self._observed_statuses_to_next_file_seeds: - self._RegenerateStatusesToFileSeeds() + file_seeds = statuses_to_file_seeds[ status ] - - indexed_file_seeds = self._statuses_to_indexed_file_seeds[ status ] - - while len( indexed_file_seeds ) > 0: - - row = indexed_file_seeds[ 0 ] - - file_seed = row[1] - - if file_seed.status == status: + if len( file_seeds ) == 0: - return file_seed + self._observed_statuses_to_next_file_seeds[ status ] = None else: - self._FixFileSeedsStatusPosition( ( file_seed, ) ) + self._observed_statuses_to_next_file_seeds[ status ] = min( file_seeds, key = lambda f_s: file_seeds_to_indices[ f_s ] ) - indexed_file_seeds = self._statuses_to_indexed_file_seeds[ status ] + + file_seed = self._observed_statuses_to_next_file_seeds[ status ] + + if file_seed is None: + + return None - return None + ( result, wrong_file_seeds ) = WalkToNextFileSeed( status, file_seed, self._file_seeds, file_seeds_to_indices, statuses_to_file_seeds ) + + self._observed_statuses_to_next_file_seeds[ status ] = result + + self._FixStatusesToFileSeeds( wrong_file_seeds ) + + return file_seed def _GetSerialisableInfo( self ): @@ -2125,14 +2231,11 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): statuses_to_counts = collections.Counter() - if self._statuses_to_indexed_file_seeds_dirty: - - self._RegenerateStatusesToFileSeeds() - + statuses_to_file_seeds = self._GetStatusesToFileSeeds() - for ( status, indexed_file_seeds ) in self._statuses_to_indexed_file_seeds.items(): + for ( status, file_seeds ) in statuses_to_file_seeds.items(): - count = len( indexed_file_seeds ) + count = len( file_seeds ) if count > 0: @@ -2143,11 +2246,51 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): return statuses_to_counts + def _GetStatusesToFileSeeds( self ) -> typing.Dict[ int, typing.Set[ FileSeed ] ]: + + file_seeds_to_indices = self._GetFileSeedsToIndices() + + if self._statuses_to_file_seeds_dirty: + + self._file_seeds_to_observed_statuses = {} + self._statuses_to_file_seeds = collections.defaultdict( set ) + self._observed_statuses_to_next_file_seeds = {} + + for ( file_seed, index ) in file_seeds_to_indices.items(): + + status = file_seed.status + + self._statuses_to_file_seeds[ status ].add( file_seed ) + self._file_seeds_to_observed_statuses[ file_seed ] = status + + if status not in self._observed_statuses_to_next_file_seeds: + + self._observed_statuses_to_next_file_seeds[ status ] = file_seed + + else: + + current_next = self._observed_statuses_to_next_file_seeds[ status ] + + if current_next is not None and index < file_seeds_to_indices[ current_next ]: + + self._observed_statuses_to_next_file_seeds[ status ] = file_seed + + + + + self._statuses_to_file_seeds_dirty = False + + + return self._statuses_to_file_seeds + + def _HasFileSeed( self, file_seed: FileSeed ): + file_seeds_to_indices = self._GetFileSeedsToIndices() + search_file_seeds = file_seed.GetSearchFileSeeds() - has_file_seed = True in ( search_file_seed in self._file_seeds_to_indices for search_file_seed in search_file_seeds ) + has_file_seed = True in ( search_file_seed in file_seeds_to_indices for search_file_seed in search_file_seeds ) return has_file_seed @@ -2158,30 +2301,30 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): self._file_seeds = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_info ) - self._FileSeedIndicesJustChanged() - - def _RegenerateStatusesToFileSeeds( self ): + def _NotifyFileSeedsUpdated( self, file_seeds: typing.Collection[ FileSeed ] ): - self._statuses_to_indexed_file_seeds = collections.defaultdict( list ) - - for ( file_seed, index ) in self._file_seeds_to_indices.items(): + if len( file_seeds ) == 0: - self._statuses_to_indexed_file_seeds[ file_seed.status ].append( ( index, file_seed ) ) + return - for indexed_file_seeds in self._statuses_to_indexed_file_seeds.values(): - - indexed_file_seeds.sort() - + HG.client_controller.pub( 'file_seed_cache_file_seeds_updated', self._file_seed_cache_key, file_seeds ) - self._statuses_to_indexed_file_seeds_dirty = False + + def _SetFileSeedsToIndicesDirty( self ): + + self._file_seeds_to_indices_dirty = True + + self._observed_statuses_to_next_file_seeds = {} def _SetStatusesToFileSeedsDirty( self ): - self._statuses_to_indexed_file_seeds_dirty = True + # this is never actually called, which is neat! I think we are 'perfect' on this thing maintaining itself after inital generation + + self._statuses_to_file_seeds_dirty = True def _SetStatusDirty( self ): @@ -2406,6 +2549,8 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): with self._lock: + file_seeds_to_indices = self._GetFileSeedsToIndices() + for file_seed in file_seeds: if self._HasFileSeed( file_seed ): @@ -2445,42 +2590,50 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): index = len( self._file_seeds ) - 1 - self._file_seeds_to_indices[ file_seed ] = index - - if not self._statuses_to_indexed_file_seeds_dirty: - - self._statuses_to_indexed_file_seeds[ file_seed.status ].append( ( index, file_seed ) ) - + file_seeds_to_indices[ file_seed ] = index + self._FixStatusesToFileSeeds( updated_or_new_file_seeds ) + self._SetStatusDirty() - self.NotifyFileSeedsUpdated( updated_or_new_file_seeds ) + self._NotifyFileSeedsUpdated( updated_or_new_file_seeds ) return len( updated_or_new_file_seeds ) def AdvanceFileSeed( self, file_seed: FileSeed ): + updated_file_seeds = [] + with self._lock: - if file_seed in self._file_seeds_to_indices: + file_seeds_to_indices = self._GetFileSeedsToIndices() + + if file_seed in file_seeds_to_indices: - index = self._file_seeds_to_indices[ file_seed ] + index = file_seeds_to_indices[ file_seed ] if index > 0: + swapped_file_seed = self._file_seeds[ index - 1 ] + self._file_seeds.remove( file_seed ) self._file_seeds.insert( index - 1, file_seed ) - - self._FileSeedIndicesJustChanged() + file_seeds_to_indices[ file_seed ] = index - 1 + file_seeds_to_indices[ swapped_file_seed ] = index + + updated_file_seeds = [ file_seed, swapped_file_seed ] + + self._FixStatusesToFileSeeds( updated_file_seeds ) + - self.NotifyFileSeedsUpdated( ( file_seed, ) ) + self._NotifyFileSeedsUpdated( updated_file_seeds ) def CanCompact( self, compact_before_this_source_time: int ): @@ -2518,49 +2671,54 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): return - new_file_seeds = HydrusSerialisable.SerialisableList() + removee_file_seeds = set() for file_seed in self._file_seeds[:-self.COMPACT_NUMBER]: still_to_do = file_seed.status == CC.STATUS_UNKNOWN still_relevant = self._GetSourceTimestampForVelocityCalculations( file_seed ) > compact_before_this_source_time - if still_to_do or still_relevant: + if not ( still_to_do or still_relevant ): - new_file_seeds.append( file_seed ) + removee_file_seeds.add( file_seed ) - new_file_seeds.extend( self._file_seeds[-self.COMPACT_NUMBER:] ) - - self._file_seeds = new_file_seeds - - self._FileSeedIndicesJustChanged() - - self._SetStatusDirty() - + + self.RemoveFileSeeds( removee_file_seeds ) def DelayFileSeed( self, file_seed: FileSeed ): + updated_file_seeds = [] + with self._lock: - if file_seed in self._file_seeds_to_indices: + file_seeds_to_indices = self._GetFileSeedsToIndices() + + if file_seed in file_seeds_to_indices: - index = self._file_seeds_to_indices[ file_seed ] + index = file_seeds_to_indices[ file_seed ] if index < len( self._file_seeds ) - 1: + swapped_file_seed = self._file_seeds[ index + 1 ] + self._file_seeds.remove( file_seed ) self._file_seeds.insert( index + 1, file_seed ) - - self._FileSeedIndicesJustChanged() + file_seeds_to_indices[ swapped_file_seed ] = index + file_seeds_to_indices[ file_seed ] = index + 1 + + updated_file_seeds = [ file_seed, swapped_file_seed ] + + self._FixStatusesToFileSeeds( updated_file_seeds ) + - self.NotifyFileSeedsUpdated( ( file_seed, ) ) + self._NotifyFileSeedsUpdated( updated_file_seeds ) def GetAPIInfoDict( self, simple: bool ): @@ -2656,8 +2814,6 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): def GetFileSeedCount( self, status: int = None ): - result = 0 - with self._lock: if status is None: @@ -2666,12 +2822,9 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): else: - if self._statuses_to_indexed_file_seeds_dirty: - - self._RegenerateStatusesToFileSeeds() - + statuses_to_file_seeds = self._GetStatusesToFileSeeds() - return len( self._statuses_to_indexed_file_seeds[ status ] ) + return len( statuses_to_file_seeds[ status ] ) @@ -2690,7 +2843,9 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): with self._lock: - return self._file_seeds_to_indices[ file_seed ] + file_seeds_to_indices = self._GetFileSeedsToIndices() + + return file_seeds_to_indices[ file_seed ] @@ -2793,74 +2948,100 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): def InsertFileSeeds( self, index: int, file_seeds: typing.Collection[ FileSeed ] ): - if len( file_seeds ) == 0: - - return 0 - - - new_file_seeds = set() + file_seeds = HydrusData.DedupeList( file_seeds ) with self._lock: - index = min( index, len( self._file_seeds ) ) + new_file_seeds = [] for file_seed in file_seeds: - if self._HasFileSeed( file_seed ) or file_seed in new_file_seeds: + file_seed.Normalise() + + if self._HasFileSeed( file_seed ): continue - file_seed.Normalise() + new_file_seeds.append( file_seed ) - new_file_seeds.add( file_seed ) + + if len( file_seeds ) == 0: + + return 0 + + + index = min( index, len( self._file_seeds ) ) + + original_insertion_index = index + + for file_seed in new_file_seeds: self._file_seeds.insert( index, file_seed ) index += 1 - self._FileSeedIndicesJustChanged() + self._SetFileSeedsToIndicesDirty() self._SetStatusDirty() + self._FixStatusesToFileSeeds( new_file_seeds ) + + updated_file_seeds = self._file_seeds[ original_insertion_index : ] + - self.NotifyFileSeedsUpdated( new_file_seeds ) + self._NotifyFileSeedsUpdated( updated_file_seeds ) return len( new_file_seeds ) def NotifyFileSeedsUpdated( self, file_seeds: typing.Collection[ FileSeed ] ): + if len( file_seeds ) == 0: + + return + + with self._lock: - if not self._statuses_to_indexed_file_seeds_dirty: - - self._FixFileSeedsStatusPosition( file_seeds ) - - - # + self._FixStatusesToFileSeeds( file_seeds ) self._SetStatusDirty() - HG.client_controller.pub( 'file_seed_cache_file_seeds_updated', self._file_seed_cache_key, file_seeds ) + self._NotifyFileSeedsUpdated( file_seeds ) - def RemoveFileSeeds( self, file_seeds: typing.Iterable[ FileSeed ] ): + def RemoveFileSeeds( self, file_seeds_to_delete: typing.Iterable[ FileSeed ] ): with self._lock: - file_seeds_to_delete = set( file_seeds ) + file_seeds_to_indices = self._GetFileSeedsToIndices() + + file_seeds_to_delete = { file_seed for file_seed in file_seeds_to_delete if file_seed in file_seeds_to_indices } + + if len( file_seeds_to_delete ) == 0: + + return + + + earliest_affected_index = min( ( file_seeds_to_indices[ file_seed ] for file_seed in file_seeds_to_delete ) ) self._file_seeds = HydrusSerialisable.SerialisableList( [ file_seed for file_seed in self._file_seeds if file_seed not in file_seeds_to_delete ] ) - self._FileSeedIndicesJustChanged() + self._SetFileSeedsToIndicesDirty() self._SetStatusDirty() + self._FixStatusesToFileSeeds( file_seeds_to_delete ) + + index_shuffled_file_seeds = self._file_seeds[ earliest_affected_index : ] + - self.NotifyFileSeedsUpdated( file_seeds_to_delete ) + updated_file_seeds = file_seeds_to_delete.union( index_shuffled_file_seeds ) + + self._NotifyFileSeedsUpdated( updated_file_seeds ) def RemoveFileSeedsByStatus( self, statuses_to_remove: typing.Collection[ int ] ): @@ -2894,8 +3075,12 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): file_seed.SetStatus( CC.STATUS_UNKNOWN ) + self._FixStatusesToFileSeeds( failed_file_seeds ) + + self._SetStatusDirty() + - self.NotifyFileSeedsUpdated( failed_file_seeds ) + self._NotifyFileSeedsUpdated( failed_file_seeds ) def RetryIgnored( self, ignored_regex = None ): @@ -2917,8 +3102,12 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): file_seed.SetStatus( CC.STATUS_UNKNOWN ) + self._FixStatusesToFileSeeds( ignored_file_seeds ) + + self._SetStatusDirty() + - self.NotifyFileSeedsUpdated( ignored_file_seeds ) + self._NotifyFileSeedsUpdated( ignored_file_seeds ) def Reverse( self ): @@ -2927,10 +3116,12 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): self._file_seeds.reverse() - self._FileSeedIndicesJustChanged() + self._SetFileSeedsToIndicesDirty() + + updated_file_seeds = list( self._file_seeds ) - self.NotifyFileSeedsUpdated( list( self._file_seeds ) ) + self._NotifyFileSeedsUpdated( updated_file_seeds ) def WorkToDo( self ): diff --git a/hydrus/client/importing/ClientImportGallerySeeds.py b/hydrus/client/importing/ClientImportGallerySeeds.py index 372b436d..5fb0a8f5 100644 --- a/hydrus/client/importing/ClientImportGallerySeeds.py +++ b/hydrus/client/importing/ClientImportGallerySeeds.py @@ -298,7 +298,7 @@ class GallerySeed( HydrusSerialisable.SerialisableBase ): return False - def WorkOnURL( self, gallery_token_name, gallery_seed_log, file_seeds_callable, status_hook, title_hook, network_job_factory, network_job_presentation_context_factory, file_import_options, gallery_urls_seen_before = None ): + def WorkOnURL( self, gallery_token_name, gallery_seed_log: "GallerySeedLog", file_seeds_callable, status_hook, title_hook, network_job_factory, network_job_presentation_context_factory, file_import_options, gallery_urls_seen_before = None ): if gallery_urls_seen_before is None: @@ -494,7 +494,7 @@ class GallerySeed( HydrusSerialisable.SerialisableBase ): sub_gallery_seed.SetExternalAdditionalServiceKeysToTags( self._external_additional_service_keys_to_tags ) - gallery_seed_log.AddGallerySeeds( sub_gallery_seeds ) + gallery_seed_log.AddGallerySeeds( sub_gallery_seeds, parent_gallery_seed = self ) added_new_gallery_pages = True @@ -569,7 +569,7 @@ class GallerySeed( HydrusSerialisable.SerialisableBase ): next_gallery_seed.SetExternalAdditionalServiceKeysToTags( self._external_additional_service_keys_to_tags ) - gallery_seed_log.AddGallerySeeds( next_gallery_seeds ) + gallery_seed_log.AddGallerySeeds( next_gallery_seeds, parent_gallery_seed = self ) added_new_gallery_pages = True @@ -760,7 +760,7 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): self._status_dirty = True - def AddGallerySeeds( self, gallery_seeds ): + def AddGallerySeeds( self, gallery_seeds, parent_gallery_seed: typing.Optional[ GallerySeed ] = None ) -> int: if len( gallery_seeds ) == 0: @@ -786,23 +786,48 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): new_gallery_seeds.append( gallery_seed ) - self._gallery_seeds.append( gallery_seed ) - - self._gallery_seeds_to_indices[ gallery_seed ] = len( self._gallery_seeds ) - 1 - seen_urls.add( gallery_seed.url ) + if len( new_gallery_seeds ) == 0: + + return 0 + + + if parent_gallery_seed is None or parent_gallery_seed not in self._gallery_seeds: + + insertion_index = len( self._gallery_seeds ) + + else: + + insertion_index = self._gallery_seeds.index( parent_gallery_seed ) + 1 + + + original_insertion_index = insertion_index + + for gallery_seed in new_gallery_seeds: + + self._gallery_seeds.insert( insertion_index, gallery_seed ) + + insertion_index += 1 + + + self._gallery_seeds_to_indices = { gallery_seed : index for ( index, gallery_seed ) in enumerate( self._gallery_seeds ) } + self._SetStatusDirty() + updated_gallery_seeds = self._gallery_seeds[ original_insertion_index : ] + - self.NotifyGallerySeedsUpdated( new_gallery_seeds ) + self.NotifyGallerySeedsUpdated( updated_gallery_seeds ) return len( new_gallery_seeds ) def AdvanceGallerySeed( self, gallery_seed ): + updated_gallery_seeds = [] + with self._lock: if gallery_seed in self._gallery_seeds_to_indices: @@ -811,16 +836,20 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): if index > 0: + swapped_gallery_seed = self._gallery_seeds[ index - 1 ] + self._gallery_seeds.remove( gallery_seed ) self._gallery_seeds.insert( index - 1, gallery_seed ) - - self._gallery_seeds_to_indices = { gallery_seed : index for ( index, gallery_seed ) in enumerate( self._gallery_seeds ) } - + self._gallery_seeds_to_indices[ gallery_seed ] = index - 1 + self._gallery_seeds_to_indices[ swapped_gallery_seed ] = index + + updated_gallery_seeds = ( gallery_seed, swapped_gallery_seed ) + - self.NotifyGallerySeedsUpdated( ( gallery_seed, ) ) + self.NotifyGallerySeedsUpdated( updated_gallery_seeds ) def CanCompact( self, compact_before_this_source_time ): @@ -900,6 +929,8 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): def DelayGallerySeed( self, gallery_seed ): + updated_gallery_seeds = [] + with self._lock: if gallery_seed in self._gallery_seeds_to_indices: @@ -908,16 +939,21 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): if index < len( self._gallery_seeds ) - 1: + swapped_gallery_seed = self._gallery_seeds[ index + 1 ] + self._gallery_seeds.remove( gallery_seed ) self._gallery_seeds.insert( index + 1, gallery_seed ) - - self._gallery_seeds_to_indices = { gallery_seed : index for ( index, gallery_seed ) in enumerate( self._gallery_seeds ) } + self._gallery_seeds_to_indices[ swapped_gallery_seed ] = index + self._gallery_seeds_to_indices[ gallery_seed ] = index + 1 + + updated_gallery_seeds = ( swapped_gallery_seed, gallery_seed ) + - self.NotifyGallerySeedsUpdated( ( gallery_seed, ) ) + self.NotifyGallerySeedsUpdated( updated_gallery_seeds ) def GetExampleGallerySeed( self ): @@ -1062,6 +1098,11 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): def NotifyGallerySeedsUpdated( self, gallery_seeds ): + if len( gallery_seeds ) == 0: + + return + + with self._lock: self._SetStatusDirty() @@ -1070,11 +1111,18 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): HG.client_controller.pub( 'gallery_seed_log_gallery_seeds_updated', self._gallery_seed_log_key, gallery_seeds ) - def RemoveGallerySeeds( self, gallery_seeds ): + def RemoveGallerySeeds( self, gallery_seeds_to_delete ): with self._lock: - gallery_seeds_to_delete = set( gallery_seeds ) + gallery_seeds_to_delete = { gallery_seed for gallery_seed in gallery_seeds_to_delete if gallery_seed in self._gallery_seeds_to_indices } + + if len( gallery_seeds_to_delete ) == 0: + + return + + + earliest_affected_index = min( ( self._gallery_seeds_to_indices[ gallery_seed ] for gallery_seed in gallery_seeds_to_delete ) ) self._gallery_seeds = HydrusSerialisable.SerialisableList( [ gallery_seed for gallery_seed in self._gallery_seeds if gallery_seed not in gallery_seeds_to_delete ] ) @@ -1082,8 +1130,12 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): self._SetStatusDirty() + index_shuffled_gallery_seeds = self._gallery_seeds[ earliest_affected_index : ] + - self.NotifyGallerySeedsUpdated( gallery_seeds_to_delete ) + updated_gallery_seeds = gallery_seeds_to_delete.union( index_shuffled_gallery_seeds ) + + self.NotifyGallerySeedsUpdated( updated_gallery_seeds ) def RemoveGallerySeedsByStatus( self, statuses_to_remove ): diff --git a/hydrus/client/importing/ClientImportLocal.py b/hydrus/client/importing/ClientImportLocal.py index b9e9657c..df0f3e8a 100644 --- a/hydrus/client/importing/ClientImportLocal.py +++ b/hydrus/client/importing/ClientImportLocal.py @@ -669,8 +669,8 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ): def _CheckFolder( self, job_key ): - - all_paths = ClientFiles.GetAllFilePaths( [ self._path ] ) + + ( all_paths, num_sidecars ) = ClientFiles.GetAllFilePaths( [ self._path ] ) all_paths = HydrusPaths.FilterFreePaths( all_paths ) diff --git a/hydrus/client/metadata/ClientMetadataMigrationExporters.py b/hydrus/client/metadata/ClientMetadataMigrationExporters.py index efa0d746..6eae7d74 100644 --- a/hydrus/client/metadata/ClientMetadataMigrationExporters.py +++ b/hydrus/client/metadata/ClientMetadataMigrationExporters.py @@ -130,7 +130,7 @@ class SingleFileMetadataExporterMediaTags( HydrusSerialisable.SerialisableBase, if len( tags ) > 0: - content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, add_content_action, ( tag, hashes ) ) for tag in rows ] + content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, add_content_action, ( tag, hashes ) ) for tag in tags ] HG.client_controller.WriteSynchronous( 'content_updates', { self._service_key : content_updates } ) diff --git a/hydrus/client/metadata/ClientMetadataMigrationImporters.py b/hydrus/client/metadata/ClientMetadataMigrationImporters.py index 33850095..263841eb 100644 --- a/hydrus/client/metadata/ClientMetadataMigrationImporters.py +++ b/hydrus/client/metadata/ClientMetadataMigrationImporters.py @@ -153,6 +153,9 @@ class SingleFileMetadataImporterMediaTags( HydrusSerialisable.SerialisableBase, tags = media_result.GetTagsManager().GetCurrent( self._service_key, ClientTags.TAG_DISPLAY_STORAGE ) + # turning ::) into :) + tags = { HydrusText.re_leading_double_colon.sub( ':', tag ) for tag in tags } + if self._string_processor.MakesChanges(): tags = self._string_processor.ProcessStrings( tags ) diff --git a/hydrus/client/networking/ClientLocalServer.py b/hydrus/client/networking/ClientLocalServer.py index 11396995..6e236b08 100644 --- a/hydrus/client/networking/ClientLocalServer.py +++ b/hydrus/client/networking/ClientLocalServer.py @@ -46,6 +46,7 @@ class HydrusServiceClientAPI( HydrusClientService ): root.putChild( b'session_key', ClientLocalServerResources.HydrusResourceClientAPIRestrictedAccountSessionKey( self._service, self._client_requests_domain ) ) root.putChild( b'verify_access_key', ClientLocalServerResources.HydrusResourceClientAPIRestrictedAccountVerify( self._service, self._client_requests_domain ) ) root.putChild( b'get_services', ClientLocalServerResources.HydrusResourceClientAPIRestrictedGetServices( self._service, self._client_requests_domain ) ) + root.putChild( b'get_service', ClientLocalServerResources.HydrusResourceClientAPIRestrictedGetService( self._service, self._client_requests_domain ) ) add_files = NoResource() @@ -63,7 +64,6 @@ class HydrusServiceClientAPI( HydrusClientService ): add_tags.putChild( b'add_tags', ClientLocalServerResources.HydrusResourceClientAPIRestrictedAddTagsAddTags( self._service, self._client_requests_domain ) ) add_tags.putChild( b'clean_tags', ClientLocalServerResources.HydrusResourceClientAPIRestrictedAddTagsCleanTags( self._service, self._client_requests_domain ) ) - add_tags.putChild( b'get_tag_services', ClientLocalServerResources.HydrusResourceClientAPIRestrictedAddTagsGetTagServices( self._service, self._client_requests_domain ) ) add_tags.putChild( b'search_tags', ClientLocalServerResources.HydrusResourceClientAPIRestrictedAddTagsSearchTags( self._service, self._client_requests_domain ) ) add_urls = NoResource() diff --git a/hydrus/client/networking/ClientLocalServerResources.py b/hydrus/client/networking/ClientLocalServerResources.py index bff6114f..4c56defa 100644 --- a/hydrus/client/networking/ClientLocalServerResources.py +++ b/hydrus/client/networking/ClientLocalServerResources.py @@ -58,12 +58,24 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set() # if a variable name isn't defined here, a GET with it won't work CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type', 'potentials_search_type', 'pixel_duplicates', 'max_hamming_distance', 'max_num_pairs' } -CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'tag_service_key', 'tag_service_key_1', 'tag_service_key_2', 'file_service_key' } -CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'file_service_name', 'tag_service_name', 'reason', 'tag_display_type', 'source_hash_type', 'desired_hash_type' } -CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'tags_1', 'tags_2', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_names_tags', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names', 'doublecheck_file_system' } -CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'hashes' } +CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'service_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'file_service_key', 'deleted_file_service_key', 'tag_service_key', 'tag_service_key_1', 'tag_service_key_2' } +CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'service_name', 'reason', 'tag_display_type', 'source_hash_type', 'desired_hash_type' } +CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'tags', 'tags_1', 'tags_2', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names', 'doublecheck_file_system' } +CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'file_service_keys', 'deleted_file_service_keys', 'hashes' } CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' } +LEGACY_CLIENT_API_SERVICE_NAME_STRING_PARAMS = { 'file_service_name', 'tag_service_name' } +CLIENT_API_STRING_PARAMS.update( LEGACY_CLIENT_API_SERVICE_NAME_STRING_PARAMS ) + +LEGACY_CLIENT_API_SERVICE_NAME_JSON_DICT_PARAMS = { 'service_names_to_tags', 'service_names_to_actions_to_tags', 'service_names_to_additional_tags' } +CLIENT_API_JSON_PARAMS.update( LEGACY_CLIENT_API_SERVICE_NAME_JSON_DICT_PARAMS ) + +def ConvertLegacyServiceNameParamToKey( param_name: str ): + + # top tier, works for service_name and service_names + return param_name.replace( 'name', 'key' ) + + def Dumps( data, mime ): if mime == HC.APPLICATION_CBOR: @@ -112,6 +124,25 @@ def CheckHashLength( hashes, hash_type = 'sha256' ): +def CheckFileService( file_service_key: bytes ): + + try: + + service = HG.client_controller.services_manager.GetService( file_service_key ) + + except: + + raise HydrusExceptions.BadRequestException( 'Could not find the file service "{}"!'.format( file_service_key.hex() ) ) + + + if service.GetServiceType() not in HC.ALL_FILE_SERVICES: + + raise HydrusExceptions.BadRequestException( 'Sorry, the service key "{}" did not give a file service!'.format( file_service_key.hex() ) ) + + + return service + + def CheckTagService( tag_service_key: bytes ): try: @@ -120,47 +151,93 @@ def CheckTagService( tag_service_key: bytes ): except: - raise HydrusExceptions.BadRequestException( 'Could not find that tag service!' ) + raise HydrusExceptions.BadRequestException( 'Could not find the tag service "{}"!'.format( tag_service_key.hex() ) ) if service.GetServiceType() not in HC.ALL_TAG_SERVICES: - raise HydrusExceptions.BadRequestException( 'Sorry, that service key did not give a tag service!' ) + raise HydrusExceptions.BadRequestException( 'Sorry, the service key "{}" did not give a tag service!'.format( tag_service_key.hex() ) ) + return service + -def ConvertServiceNamesDictToKeys( allowed_service_types, service_name_dict ): +def GetServiceKeyFromName( service_name: str ): - service_key_dict = {} - - for ( service_name, value ) in service_name_dict.items(): + try: - try: - - service_key = HG.client_controller.services_manager.GetServiceKeyFromName( allowed_service_types, service_name ) - - except: - - raise HydrusExceptions.BadRequestException( 'Could not find the service "{}", or it was the wrong type!'.format( service_name ) ) - + service_key = HG.client_controller.services_manager.GetServiceKeyFromName( HC.ALL_SERVICES, service_name ) - service_key_dict[ service_key ] = value + except HydrusExceptions.DataMissing: + + raise HydrusExceptions.NotFoundException( 'Sorry, did not find a service with name "{}"!'.format( service_name ) ) - return service_key_dict + return service_key + def ParseLocalBooruGETArgs( requests_args ): args = HydrusNetworkVariableHandling.ParseTwistedRequestGETArgs( requests_args, LOCAL_BOORU_INT_PARAMS, LOCAL_BOORU_BYTE_PARAMS, LOCAL_BOORU_STRING_PARAMS, LOCAL_BOORU_JSON_PARAMS, LOCAL_BOORU_JSON_BYTE_LIST_PARAMS ) return args + +def ParseClientLegacyArgs( args: dict ): + + # adding this v514, so delete when appropriate + + parsed_request_args = HydrusNetworkVariableHandling.ParsedRequestArguments( args ) + + legacy_service_string_param_names = LEGACY_CLIENT_API_SERVICE_NAME_STRING_PARAMS.intersection( parsed_request_args.keys() ) + + for legacy_service_string_param_name in legacy_service_string_param_names: + + service_name = parsed_request_args[ legacy_service_string_param_name ] + + service_key = GetServiceKeyFromName( service_name ) + + del parsed_request_args[ legacy_service_string_param_name ] + + new_service_bytes_param_name = ConvertLegacyServiceNameParamToKey( legacy_service_string_param_name ) + + parsed_request_args[ new_service_bytes_param_name ] = service_key + + + legacy_service_dict_param_names = LEGACY_CLIENT_API_SERVICE_NAME_JSON_DICT_PARAMS.intersection( parsed_request_args.keys() ) + + for legacy_service_dict_param_name in legacy_service_dict_param_names: + + service_keys_to_gubbins = {} + + service_names_to_gubbins = parsed_request_args[ legacy_service_dict_param_name ] + + for ( service_name, gubbins ) in service_names_to_gubbins.items(): + + service_key = GetServiceKeyFromName( service_name ) + + service_keys_to_gubbins[ service_key ] = gubbins + + + del parsed_request_args[ legacy_service_dict_param_name ] + + new_service_dict_param_name = ConvertLegacyServiceNameParamToKey( legacy_service_dict_param_name ) + + parsed_request_args[ new_service_dict_param_name ] = service_keys_to_gubbins + + + return parsed_request_args + + def ParseClientAPIGETArgs( requests_args ): args = HydrusNetworkVariableHandling.ParseTwistedRequestGETArgs( requests_args, CLIENT_API_INT_PARAMS, CLIENT_API_BYTE_PARAMS, CLIENT_API_STRING_PARAMS, CLIENT_API_JSON_PARAMS, CLIENT_API_JSON_BYTE_LIST_PARAMS ) + args = ParseClientLegacyArgs( args ) + return args + def ParseClientAPIPOSTByteArgs( args ): if not isinstance( args, dict ): @@ -286,6 +363,8 @@ def ParseClientAPIPOSTByteArgs( args ): + parsed_request_args = ParseClientLegacyArgs( parsed_request_args ) + return parsed_request_args def ParseClientAPIPOSTArgs( request ): @@ -342,7 +421,6 @@ def ParseClientAPIPOSTArgs( request ): parsed_request_args = ParseClientAPIPOSTByteArgs( args ) - elif request_content_type_mime == HC.APPLICATION_CBOR: if not CBOR_AVAILABLE: @@ -357,7 +435,7 @@ def ParseClientAPIPOSTArgs( request ): args = cbor2.loads( cbor_bytes ) parsed_request_args = ParseClientAPIPOSTByteArgs( args ) - + else: parsed_request_args = HydrusNetworkVariableHandling.ParsedRequestArguments() @@ -385,8 +463,6 @@ def ParseClientAPISearchPredicates( request ) -> typing.List[ ClientSearch.Predi default_search_values = {} default_search_values[ 'tags' ] = [] - default_search_values[ 'system_inbox' ] = False - default_search_values[ 'system_archive' ] = False for ( key, value ) in default_search_values.items(): @@ -396,22 +472,10 @@ def ParseClientAPISearchPredicates( request ) -> typing.List[ ClientSearch.Predi - system_inbox = request.parsed_request_args[ 'system_inbox' ] - system_archive = request.parsed_request_args[ 'system_archive' ] - tags = request.parsed_request_args[ 'tags' ] predicates = ConvertTagListToPredicates( request, tags ) - if system_inbox: - - predicates.append( ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_INBOX ) ) - - elif system_archive: - - predicates.append( ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_ARCHIVE ) ) - - if len( predicates ) == 0: return predicates @@ -436,9 +500,7 @@ def ParseClientAPISearchPredicates( request ) -> typing.List[ ClientSearch.Predi def ParseDuplicateSearch( request: HydrusServerRequest.HydrusRequest ): - # TODO: When we have ParseLocationContext for clever file searching, swap it in here too - # LocationContext has to be the same for both searches - location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) + location_context = ParseLocationContext( request, ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) ) tag_service_key_1 = request.parsed_request_args.GetValue( 'tag_service_key_1', bytes, default_value = CC.COMBINED_TAG_SERVICE_KEY ) tag_service_key_2 = request.parsed_request_args.GetValue( 'tag_service_key_2', bytes, default_value = CC.COMBINED_TAG_SERVICE_KEY ) @@ -487,43 +549,55 @@ def ParseDuplicateSearch( request: HydrusServerRequest.HydrusRequest ): ) -def ParseLocationContext( request: HydrusServerRequest.HydrusRequest, default: ClientLocation.LocationContext ): +def ParseLocationContext( request: HydrusServerRequest.HydrusRequest, default: ClientLocation.LocationContext, deleted_allowed = True ): - if 'file_service_key' in request.parsed_request_args or 'file_service_name' in request.parsed_request_args: + current_file_service_keys = set() + deleted_file_service_keys = set() + + if 'file_service_key' in request.parsed_request_args: - if 'file_service_key' in request.parsed_request_args: + file_service_key = request.parsed_request_args.GetValue( 'file_service_key', bytes ) + + current_file_service_keys.add( file_service_key ) + + + if 'file_service_keys' in request.parsed_request_args: + + file_service_keys = request.parsed_request_args.GetValue( 'file_service_keys', list, expected_list_type = bytes ) + + current_file_service_keys.update( file_service_keys ) + + + if deleted_allowed: + + if 'deleted_file_service_key' in request.parsed_request_args: - file_service_key = request.parsed_request_args[ 'file_service_key' ] + file_service_key = request.parsed_request_args.GetValue( 'deleted_file_service_key', bytes ) - else: - - file_service_name = request.parsed_request_args[ 'file_service_name' ] - - try: - - file_service_key = HG.client_controller.services_manager.GetServiceKeyFromName( HC.ALL_FILE_SERVICES, file_service_name ) - - except: - - raise HydrusExceptions.BadRequestException( 'Could not find the service "{}"!'.format( file_service_name ) ) - + deleted_file_service_keys.add( file_service_key ) - try: + if 'deleted_file_service_keys' in request.parsed_request_args: - service_type = HG.client_controller.services_manager.GetServiceType( file_service_key ) + file_service_keys = request.parsed_request_args.GetValue( 'deleted_file_service_keys', list, expected_list_type = bytes ) - except: - - raise HydrusExceptions.BadRequestException( 'Could not find that file service!' ) + deleted_file_service_keys.update( file_service_keys ) - if service_type not in HC.ALL_FILE_SERVICES: - - raise HydrusExceptions.BadRequestException( 'Sorry, that service key did not give a file service!' ) - + + for service_key in current_file_service_keys: - return ClientLocation.LocationContext.STATICCreateSimple( file_service_key ) + CheckFileService( service_key ) + + + for service_key in deleted_file_service_keys: + + CheckFileService( service_key ) + + + if len( current_file_service_keys ) > 0 or len( deleted_file_service_keys ) > 0: + + return ClientLocation.LocationContext( current_service_keys = current_file_service_keys, deleted_service_keys = deleted_file_service_keys ) else: @@ -533,40 +607,60 @@ def ParseLocationContext( request: HydrusServerRequest.HydrusRequest, default: C def ParseHashes( request: HydrusServerRequest.HydrusRequest ): - hashes = set() + something_was_set = False + + hashes = [] if 'hash' in request.parsed_request_args: + something_was_set = True + hash = request.parsed_request_args.GetValue( 'hash', bytes ) - hashes.add( hash ) + hashes.append( hash ) if 'hashes' in request.parsed_request_args: + something_was_set = True + more_hashes = request.parsed_request_args.GetValue( 'hashes', list, expected_list_type = bytes ) - hashes.update( more_hashes ) + hashes.extend( more_hashes ) if 'file_id' in request.parsed_request_args: + something_was_set = True + hash_id = request.parsed_request_args.GetValue( 'file_id', int ) hash_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = [ hash_id ] ) - hashes.update( hash_ids_to_hashes.values() ) + if len( hash_ids_to_hashes ) > 0: + + hashes.extend( hash_ids_to_hashes[ hash_id ] ) + if 'file_ids' in request.parsed_request_args: + something_was_set = True + hash_ids = request.parsed_request_args.GetValue( 'file_ids', list, expected_list_type = int ) hash_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = hash_ids ) - hashes.update( hash_ids_to_hashes.values() ) + hashes.extend( [ hash_ids_to_hashes[ hash_id ] for hash_id in hash_ids ] ) + if not something_was_set: # subtly different to 'no hashes' + + raise HydrusExceptions.BadRequestException( 'Please include some files in your request--file_id or hash based!' ) + + + hashes = HydrusData.DedupeList( hashes ) + CheckHashLength( hashes ) return hashes @@ -619,25 +713,12 @@ def ParseRequestedResponseMime( request: HydrusServerRequest.HydrusRequest ): def ParseTagServiceKey( request: HydrusServerRequest.HydrusRequest ): - if 'tag_service_key' in request.parsed_request_args or 'tag_service_name' in request.parsed_request_args: + if 'tag_service_key' in request.parsed_request_args: if 'tag_service_key' in request.parsed_request_args: tag_service_key = request.parsed_request_args[ 'tag_service_key' ] - else: - - tag_service_name = request.parsed_request_args[ 'tag_service_name' ] - - try: - - tag_service_key = HG.client_controller.services_manager.GetServiceKeyFromName( HC.ALL_TAG_SERVICES, tag_service_name ) - - except: - - raise HydrusExceptions.BadRequestException( 'Could not find the service "{}"!'.format( tag_service_name ) ) - - CheckTagService( tag_service_key ) @@ -1344,6 +1425,90 @@ class HydrusResourceClientAPIRestrictedAccountVerify( HydrusResourceClientAPIRes return response_context +class HydrusResourceClientAPIRestrictedGetService( HydrusResourceClientAPIRestricted ): + + def _CheckAPIPermissions( self, request: HydrusServerRequest.HydrusRequest ): + + request.client_api_permissions.CheckAtLeastOnePermission( + ( + ClientAPI.CLIENT_API_PERMISSION_ADD_FILES, + ClientAPI.CLIENT_API_PERMISSION_ADD_TAGS, + ClientAPI.CLIENT_API_PERMISSION_ADD_NOTES, + ClientAPI.CLIENT_API_PERMISSION_MANAGE_PAGES, + ClientAPI.CLIENT_API_PERMISSION_MANAGE_FILE_RELATIONSHIPS, + ClientAPI.CLIENT_API_PERMISSION_SEARCH_FILES + ) + ) + + + def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): + + allowed_service_types = { + HC.LOCAL_TAG, + HC.TAG_REPOSITORY, + HC.LOCAL_FILE_DOMAIN, + HC.LOCAL_FILE_UPDATE_DOMAIN, + HC.FILE_REPOSITORY, + HC.COMBINED_LOCAL_FILE, + HC.COMBINED_LOCAL_MEDIA, + HC.COMBINED_FILE, + HC.COMBINED_TAG, + HC.LOCAL_FILE_TRASH_DOMAIN + } + + if 'service_key' in request.parsed_request_args: + + service_key = request.parsed_request_args.GetValue( 'service_key', bytes ) + + elif 'service_name' in request.parsed_request_args: + + service_name = request.parsed_request_args.GetValue( 'service_name', str ) + + try: + + service_key = HG.client_controller.services_manager.GetServiceKeyFromName( allowed_service_types, service_name ) + + except HydrusExceptions.DataMissing: + + raise HydrusExceptions.NotFoundException( 'Sorry, did not find a service with name "{}"!'.format( service_name ) ) + + + else: + + raise HydrusExceptions.BadRequestException( 'Sorry, you need to give a service_key or service_name!' ) + + + try: + + service = HG.client_controller.services_manager.GetService( service_key ) + + except HydrusExceptions.DataMissing: + + raise HydrusExceptions.NotFoundException( 'Sorry, did not find a service with key "{}"!'.format( service_key.hex() ) ) + + + if service.GetServiceType() not in allowed_service_types: + + raise HydrusExceptions.BadRequestException( 'Sorry, for now, you cannot ask about this service!' ) + + + body_dict = { + 'service' : { + 'name' : service.GetName(), + 'type' : service.GetServiceType(), + 'type_pretty' : HC.service_string_lookup[ service.GetServiceType() ], + 'service_key' : service.GetServiceKey().hex() + } + } + + body = Dumps( body_dict, request.preferred_mime ) + + response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body ) + + return response_context + + + class HydrusResourceClientAPIRestrictedGetServices( HydrusResourceClientAPIRestricted ): def _CheckAPIPermissions( self, request: HydrusServerRequest.HydrusRequest ): @@ -1464,7 +1629,7 @@ class HydrusResourceClientAPIRestrictedAddFilesArchiveFiles( HydrusResourceClien def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): - hashes = ParseHashes( request ) + hashes = set( ParseHashes( request ) ) content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, hashes ) @@ -1484,7 +1649,7 @@ class HydrusResourceClientAPIRestrictedAddFilesDeleteFiles( HydrusResourceClient def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): - location_context = ParseLocationContext( request, ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) ) + location_context = ParseLocationContext( request, ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ), deleted_allowed = False ) if 'reason' in request.parsed_request_args: @@ -1495,7 +1660,7 @@ class HydrusResourceClientAPIRestrictedAddFilesDeleteFiles( HydrusResourceClient reason = 'Deleted via Client API.' - hashes = ParseHashes( request ) + hashes = set( ParseHashes( request ) ) # expand this to take reason @@ -1519,7 +1684,7 @@ class HydrusResourceClientAPIRestrictedAddFilesUnarchiveFiles( HydrusResourceCli def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): - hashes = ParseHashes( request ) + hashes = set( ParseHashes( request ) ) content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_INBOX, hashes ) @@ -1538,7 +1703,7 @@ class HydrusResourceClientAPIRestrictedAddFilesUndeleteFiles( HydrusResourceClie location_context = ParseLocationContext( request, ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) ) - hashes = ParseHashes( request ) + hashes = set( ParseHashes( request ) ) location_context.LimitToServiceTypes( HG.client_controller.services_manager.GetServiceType, ( HC.LOCAL_FILE_DOMAIN, HC.COMBINED_LOCAL_MEDIA ) ) @@ -1677,38 +1842,31 @@ class HydrusResourceClientAPIRestrictedAddTagsAddTags( HydrusResourceClientAPIRe def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): - hashes = ParseHashes( request ) + hashes = set( ParseHashes( request ) ) # service_keys_to_tags = None + service_keys_to_actions_to_tags = None + if 'service_keys_to_tags' in request.parsed_request_args: service_keys_to_tags = request.parsed_request_args.GetValue( 'service_keys_to_tags', dict ) - elif 'service_names_to_tags' in request.parsed_request_args: - - service_names_to_tags = request.parsed_request_args.GetValue( 'service_names_to_tags', dict ) - - service_keys_to_tags = ConvertServiceNamesDictToKeys( HC.REAL_TAG_SERVICES, service_names_to_tags ) - - - service_keys_to_actions_to_tags = None - - if service_keys_to_tags is not None: - service_keys_to_actions_to_tags = {} for ( service_key, tags ) in service_keys_to_tags.items(): - try: + service = CheckTagService( service_key ) + + HydrusNetworkVariableHandling.TestVariableType( 'tags in service_keys_to_tags', tags, list, expected_list_type = str ) + + tags = HydrusTags.CleanTags( tags ) + + if len( tags ) == 0: - service = HG.client_controller.services_manager.GetService( service_key ) - - except: - - raise HydrusExceptions.BadRequestException( 'Could not find the service with key {}! Maybe it was recently deleted?'.format( service_key.hex() ) ) + continue if service.GetServiceType() == HC.LOCAL_TAG: @@ -1728,13 +1886,58 @@ class HydrusResourceClientAPIRestrictedAddTagsAddTags( HydrusResourceClientAPIRe if 'service_keys_to_actions_to_tags' in request.parsed_request_args: - service_keys_to_actions_to_tags = request.parsed_request_args.GetValue( 'service_keys_to_actions_to_tags', dict ) + parsed_service_keys_to_actions_to_tags = request.parsed_request_args.GetValue( 'service_keys_to_actions_to_tags', dict ) - elif 'service_names_to_actions_to_tags' in request.parsed_request_args: + service_keys_to_actions_to_tags = {} - service_names_to_actions_to_tags = request.parsed_request_args.GetValue( 'service_names_to_actions_to_tags', dict ) - - service_keys_to_actions_to_tags = ConvertServiceNamesDictToKeys( HC.REAL_TAG_SERVICES, service_names_to_actions_to_tags ) + for ( service_key, parsed_actions_to_tags ) in parsed_service_keys_to_actions_to_tags.items(): + + service = CheckTagService( service_key ) + + HydrusNetworkVariableHandling.TestVariableType( 'actions_to_tags', parsed_actions_to_tags, dict ) + + actions_to_tags = {} + + for ( parsed_content_action, tags ) in parsed_actions_to_tags.items(): + + HydrusNetworkVariableHandling.TestVariableType( 'action in actions_to_tags', parsed_content_action, str ) + + try: + + content_action = int( parsed_content_action ) + + except: + + raise HydrusExceptions.BadRequestException( 'Sorry, got an action, "{}", that was not an integer!'.format( parsed_content_action ) ) + + + if service.GetServiceType() == HC.LOCAL_TAG: + + if content_action not in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE ): + + raise HydrusExceptions.BadRequestException( 'Sorry, you submitted a content action of "{}" for service "{}", but you can only add/delete on a local tag service!'.format( parsed_content_action, service_key.hex() ) ) + + + else: + + if content_action in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE ): + + raise HydrusExceptions.BadRequestException( 'Sorry, you submitted a content action of "{}" for service "{}", but you cannot add/delete on a remote tag service!'.format( parsed_content_action, service_key.hex() ) ) + + + + HydrusNetworkVariableHandling.TestVariableType( 'tags in actions_to_tags', tags, list ) # do not test for str here, it can be reason tuples! + + actions_to_tags[ content_action ] = tags + + + if len( actions_to_tags ) == 0: + + continue + + + service_keys_to_actions_to_tags[ service_key ] = actions_to_tags + if service_keys_to_actions_to_tags is None: @@ -1746,32 +1949,13 @@ class HydrusResourceClientAPIRestrictedAddTagsAddTags( HydrusResourceClientAPIRe for ( service_key, actions_to_tags ) in service_keys_to_actions_to_tags.items(): - try: - - service = HG.client_controller.services_manager.GetService( service_key ) - - except HydrusExceptions.DataMissing: - - raise HydrusExceptions.BadRequestException( 'Could not find the service with key {}! Maybe it was recently deleted?'.format( service_key.hex() ) ) - - - if service.GetServiceType() not in HC.REAL_TAG_SERVICES: - - raise HydrusExceptions.BadRequestException( 'Was given a service that is not a tag service!' ) - - for ( content_action, tags ) in actions_to_tags.items(): tags = list( tags ) - if len( tags ) == 0: - - continue - - content_action = int( content_action ) - actual_tags = [] + content_update_tags = [] tags_to_reasons = {} @@ -1797,41 +1981,31 @@ class HydrusResourceClientAPIRestrictedAddTagsAddTags( HydrusResourceClientAPIRe continue - actual_tags.append( tag ) + try: + + tag = HydrusTags.CleanTag( tag ) + + except: + + continue + + + content_update_tags.append( tag ) tags_to_reasons[ tag ] = reason - actual_tags = HydrusTags.CleanTags( actual_tags ) - - if len( actual_tags ) == 0: + if len( content_update_tags ) == 0: continue - tags = actual_tags - - if service.GetServiceType() == HC.LOCAL_TAG: - - if content_action not in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE ): - - continue - - - else: - - if content_action in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE ): - - continue - - - if content_action == HC.CONTENT_UPDATE_PETITION: - content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_action, ( tag, hashes ), reason = tags_to_reasons[ tag ] ) for tag in tags ] + content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_action, ( tag, hashes ), reason = tags_to_reasons[ tag ] ) for tag in content_update_tags ] else: - content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_action, ( tag, hashes ) ) for tag in tags ] + content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_action, ( tag, hashes ) ) for tag in content_update_tags ] service_keys_to_content_updates[ service_key ].extend( content_updates ) @@ -1848,25 +2022,7 @@ class HydrusResourceClientAPIRestrictedAddTagsAddTags( HydrusResourceClientAPIRe return response_context -class HydrusResourceClientAPIRestrictedAddTagsGetTagServices( HydrusResourceClientAPIRestrictedAddTags ): - - def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): - - local_tags = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_TAG, ) ) - tag_repos = HG.client_controller.services_manager.GetServices( ( HC.TAG_REPOSITORY, ) ) - - body_dict = {} - - body_dict[ 'local_tags' ] = [ service.GetName() for service in local_tags ] - body_dict[ 'tag_repositories' ] = [ service.GetName() for service in tag_repos ] - - body = Dumps( body_dict, request.preferred_mime ) - - response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body ) - - return response_context - - + class HydrusResourceClientAPIRestrictedAddTagsSearchTags( HydrusResourceClientAPIRestrictedAddTags ): def _CheckAPIPermissions( self, request: HydrusServerRequest.HydrusRequest ): @@ -1897,9 +2053,9 @@ class HydrusResourceClientAPIRestrictedAddTagsSearchTags( HydrusResourceClientAP autocomplete_search_text = parsed_autocomplete_text.GetSearchText( True ) - default_location_context = HG.client_controller.new_options.GetDefaultLocalLocationContext() + location_context = ParseLocationContext( request, ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) ) - file_search_context = ClientSearch.FileSearchContext( location_context = default_location_context, tag_context = tag_context ) + file_search_context = ClientSearch.FileSearchContext( location_context = location_context, tag_context = tag_context ) job_key = ClientThreading.JobKey( cancellable = True ) @@ -1995,15 +2151,7 @@ class HydrusResourceClientAPIRestrictedAddURLsAssociateURL( HydrusResourceClient urls = request.parsed_request_args.GetValue( 'urls_to_add', list, expected_list_type = str ) - for url in urls: - - if not isinstance( url, str ): - - continue - - - urls_to_add.append( url ) - + urls_to_add.extend( urls ) urls_to_delete = [] @@ -2021,12 +2169,7 @@ class HydrusResourceClientAPIRestrictedAddURLsAssociateURL( HydrusResourceClient for url in urls: - if not isinstance( url, str ): - - continue - - - urls_to_delete.append( url ) + urls_to_delete.extend( urls ) @@ -2046,7 +2189,7 @@ class HydrusResourceClientAPIRestrictedAddURLsAssociateURL( HydrusResourceClient raise HydrusExceptions.BadRequestException( 'Did not find any URLs to add or delete!' ) - applicable_hashes = ParseHashes( request ) + applicable_hashes = set( ParseHashes( request ) ) if len( applicable_hashes ) == 0: @@ -2187,38 +2330,15 @@ class HydrusResourceClientAPIRestrictedAddURLsImportURL( HydrusResourceClientAPI additional_service_keys_to_tags = ClientTags.ServiceKeysToTags() - service_keys_to_additional_tags = None - - if 'service_names_to_tags' in request.parsed_request_args or 'service_names_to_additional_tags' in request.parsed_request_args: - - if 'service_names_to_tags' in request.parsed_request_args: - - service_names_to_additional_tags = request.parsed_request_args.GetValue( 'service_names_to_tags', dict ) - - else: - - service_names_to_additional_tags = request.parsed_request_args.GetValue( 'service_names_to_additional_tags', dict ) - - - service_keys_to_additional_tags = ConvertServiceNamesDictToKeys( HC.REAL_TAG_SERVICES, service_names_to_additional_tags ) - - elif 'service_keys_to_additional_tags' in request.parsed_request_args: + if 'service_keys_to_additional_tags' in request.parsed_request_args: service_keys_to_additional_tags = request.parsed_request_args.GetValue( 'service_keys_to_additional_tags', dict ) - - if service_keys_to_additional_tags is not None: - request.client_api_permissions.CheckPermission( ClientAPI.CLIENT_API_PERMISSION_ADD_TAGS ) for ( service_key, tags ) in service_keys_to_additional_tags.items(): - service = HG.client_controller.services_manager.GetService( service_key ) - - if service.GetServiceType() not in HC.REAL_TAG_SERVICES: - - raise HydrusExceptions.BadRequestException( 'Was given a service that is not a tag service!' ) - + CheckTagService( service_key ) tags = HydrusTags.CleanTags( tags ) @@ -2487,78 +2607,22 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): - missing_hashes = set() - only_return_identifiers = request.parsed_request_args.GetValue( 'only_return_identifiers', bool, default_value = False ) only_return_basic_information = request.parsed_request_args.GetValue( 'only_return_basic_information', bool, default_value = False ) - hide_service_names_tags = request.parsed_request_args.GetValue( 'hide_service_names_tags', bool, default_value = True ) - hide_service_keys_tags = request.parsed_request_args.GetValue( 'hide_service_names_tags', bool, default_value = False ) + hide_service_keys_tags = request.parsed_request_args.GetValue( 'hide_service_keys_tags', bool, default_value = True ) detailed_url_information = request.parsed_request_args.GetValue( 'detailed_url_information', bool, default_value = False ) include_notes = request.parsed_request_args.GetValue( 'include_notes', bool, default_value = False ) create_new_file_ids = request.parsed_request_args.GetValue( 'create_new_file_ids', bool, default_value = False ) - if 'file_ids' in request.parsed_request_args or 'file_id' in request.parsed_request_args: - - if 'file_ids' in request.parsed_request_args: - - file_ids = request.parsed_request_args.GetValue( 'file_ids', list, expected_list_type = int ) - - else: - - file_ids = [ request.parsed_request_args.GetValue( 'file_id', int ) ] - - request.client_api_permissions.CheckPermissionToSeeFiles( file_ids ) - - elif 'hashes' in request.parsed_request_args or 'hash' in request.parsed_request_args: - - request.client_api_permissions.CheckCanSeeAllFiles() - - if 'hashes' in request.parsed_request_args: - - hashes = request.parsed_request_args.GetValue( 'hashes', list, expected_list_type = bytes ) - - else: - - hashes = [ request.parsed_request_args.GetValue( 'hash', bytes ) ] - - - hashes = HydrusData.DedupeList( hashes ) - - CheckHashLength( hashes ) - - file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hashes = hashes, create_new_hash_ids = create_new_file_ids ) - - file_ids = set( file_ids_to_hashes.keys() ) - - if len( file_ids_to_hashes ) < len( hashes ): - - missing_hashes = set( hashes ).difference( file_ids_to_hashes.values() ) - - - else: - - raise HydrusExceptions.BadRequestException( 'Please include a file_ids or hashes parameter!' ) - + hashes = ParseHashes( request ) - try: - - if only_return_identifiers: - - file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = file_ids ) - - elif only_return_basic_information: - - file_info_managers = HG.client_controller.Read( 'file_info_managers_from_ids', file_ids, sorted = True ) - - else: - - media_results = HG.client_controller.Read( 'media_results_from_ids', file_ids, sorted = True ) - - - except HydrusExceptions.DataMissing as e: - - raise HydrusExceptions.NotFoundException( 'One or more of those file identifiers did not exist in the database!' ) - + file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hashes = hashes, create_new_hash_ids = create_new_file_ids ) + + missing_hashes = set( hashes ).difference( file_ids_to_hashes.values() ) + + file_ids = set( file_ids_to_hashes.keys() ) + + request.client_api_permissions.CheckPermissionToSeeFiles( file_ids ) body_dict = {} @@ -2588,6 +2652,8 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien elif only_return_basic_information: + file_info_managers = HG.client_controller.Read( 'file_info_managers_from_ids', file_ids, sorted = True ) + for file_info_manager in file_info_managers: metadata_row = { @@ -2609,6 +2675,8 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien else: + media_results = HG.client_controller.Read( 'media_results_from_ids', file_ids, sorted = True ) + services_manager = HG.client_controller.services_manager tag_service_keys = services_manager.GetServiceKeys( HC.ALL_TAG_SERVICES ) @@ -2783,7 +2851,6 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien # Old stuff starts here - service_names_to_statuses_to_tags = {} api_service_keys_to_statuses_to_tags = {} service_keys_to_statuses_to_tags = tags_manager.GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_STORAGE ) @@ -2794,19 +2861,10 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien if len( statuses_to_tags_json_serialisable ) > 0: - service_name = service_keys_to_names[ service_key ] - - service_names_to_statuses_to_tags[ service_name ] = statuses_to_tags_json_serialisable - api_service_keys_to_statuses_to_tags[ service_key.hex() ] = statuses_to_tags_json_serialisable - if not hide_service_names_tags: - - metadata_row[ 'service_names_to_statuses_to_tags' ] = service_names_to_statuses_to_tags - - if not hide_service_keys_tags: metadata_row[ 'service_keys_to_statuses_to_tags' ] = api_service_keys_to_statuses_to_tags @@ -2814,7 +2872,6 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien # - service_names_to_statuses_to_tags = {} api_service_keys_to_statuses_to_tags = {} service_keys_to_statuses_to_tags = tags_manager.GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_ACTUAL ) @@ -2825,19 +2882,10 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien if len( statuses_to_tags_json_serialisable ) > 0: - service_name = service_keys_to_names[ service_key ] - - service_names_to_statuses_to_tags[ service_name ] = statuses_to_tags_json_serialisable - api_service_keys_to_statuses_to_tags[ service_key.hex() ] = statuses_to_tags_json_serialisable - if not hide_service_names_tags: - - metadata_row[ 'service_names_to_statuses_to_display_tags' ] = service_names_to_statuses_to_tags - - if not hide_service_keys_tags: metadata_row[ 'service_keys_to_statuses_to_display_tags' ] = api_service_keys_to_statuses_to_tags @@ -2861,6 +2909,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien return response_context + class HydrusResourceClientAPIRestrictedGetFilesGetThumbnail( HydrusResourceClientAPIRestrictedGetFiles ): def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): @@ -3147,8 +3196,7 @@ class HydrusResourceClientAPIRestrictedManageFileRelationshipsGetRelationships( def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): - # TODO: When we have ParseLocationContext for clever file searching, swap it in here too - location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) + location_context = ParseLocationContext( request, ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) ) hashes = ParseHashes( request ) @@ -3262,21 +3310,59 @@ class HydrusResourceClientAPIRestrictedManageFileRelationshipsSetRelationships( def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): - rows = [] + database_write_rows = [] - raw_rows = request.parsed_request_args.GetValue( 'pair_rows', list, expected_list_type = list ) + raw_rows = [] all_hashes = set() - for row in raw_rows: + pair_rows_old_arg_raw_rows = request.parsed_request_args.GetValue( 'pair_rows', list, expected_list_type = list, default_value = [] ) + + for row in pair_rows_old_arg_raw_rows: if len( row ) != 6: raise HydrusExceptions.BadRequestException( 'One of the pair rows was the wrong length!' ) + raw_rows.append( row ) + + + raw_relationship_dicts = request.parsed_request_args.GetValue( 'relationships', list, expected_list_type = dict, default_value = [] ) + + for raw_relationship_dict in raw_relationship_dicts: + + duplicate_type = HydrusNetworkVariableHandling.GetValueFromDict( raw_relationship_dict, 'relationship', int ) + hash_a_hex = HydrusNetworkVariableHandling.GetValueFromDict( raw_relationship_dict, 'hash_a', str ) + hash_b_hex = HydrusNetworkVariableHandling.GetValueFromDict( raw_relationship_dict, 'hash_b', str ) + do_default_content_merge = HydrusNetworkVariableHandling.GetValueFromDict( raw_relationship_dict, 'do_default_content_merge', bool ) + delete_a = HydrusNetworkVariableHandling.GetValueFromDict( raw_relationship_dict, 'delete_a', bool, default_value = False ) + delete_b = HydrusNetworkVariableHandling.GetValueFromDict( raw_relationship_dict, 'delete_b', bool, default_value = False ) + + raw_rows.append( ( duplicate_type, hash_a_hex, hash_b_hex, do_default_content_merge, delete_a, delete_b ) ) + + + allowed_duplicate_types = { + HC.DUPLICATE_FALSE_POSITIVE, + HC.DUPLICATE_ALTERNATE, + HC.DUPLICATE_BETTER, + HC.DUPLICATE_WORSE, + HC.DUPLICATE_SAME_QUALITY, + HC.DUPLICATE_POTENTIAL + } + + # variable type testing + for row in raw_rows: + ( duplicate_type, hash_a_hex, hash_b_hex, do_default_content_merge, delete_first, delete_second ) = row + HydrusNetworkVariableHandling.TestVariableType( 'relationship', duplicate_type, int, allowed_values = allowed_duplicate_types ) + HydrusNetworkVariableHandling.TestVariableType( 'hash_a', hash_a_hex, str ) + HydrusNetworkVariableHandling.TestVariableType( 'hash_b', hash_b_hex, str ) + HydrusNetworkVariableHandling.TestVariableType( 'do_default_content_merge', do_default_content_merge, bool ) + HydrusNetworkVariableHandling.TestVariableType( 'delete_first', delete_first, bool ) + HydrusNetworkVariableHandling.TestVariableType( 'delete_second', delete_second, bool ) + try: hash_a = bytes.fromhex( hash_a_hex ) @@ -3300,44 +3386,8 @@ class HydrusResourceClientAPIRestrictedManageFileRelationshipsSetRelationships( ( duplicate_type, hash_a_hex, hash_b_hex, do_default_content_merge, delete_first, delete_second ) = row - if duplicate_type not in [ - HC.DUPLICATE_FALSE_POSITIVE, - HC.DUPLICATE_ALTERNATE, - HC.DUPLICATE_BETTER, - HC.DUPLICATE_WORSE, - HC.DUPLICATE_SAME_QUALITY, - HC.DUPLICATE_POTENTIAL - ]: - - raise HydrusExceptions.BadRequestException( 'One of the duplicate statuses ({}) was incorrect!'.format( duplicate_type ) ) - - - try: - - hash_a = bytes.fromhex( hash_a_hex ) - hash_b = bytes.fromhex( hash_b_hex ) - - except: - - raise HydrusExceptions.BadRequestException( 'Sorry, did not understand one of the hashes {} or {}!'.format( hash_a_hex, hash_b_hex ) ) - - - if not isinstance( do_default_content_merge, bool ): - - raise HydrusExceptions.BadRequestException( 'Sorry, "do_default_content_merge" has to be a boolean! "{}" was not!'.format( do_default_content_merge ) ) - - - if not isinstance( delete_first, bool ): - - raise HydrusExceptions.BadRequestException( 'Sorry, "delete_first" has to be a boolean! "{}" was not!'.format( delete_first ) ) - - - if not isinstance( delete_second, bool ): - - raise HydrusExceptions.BadRequestException( 'Sorry, "delete_second" has to be a boolean! "{}" was not!'.format( delete_second ) ) - - - # ok the raw row looks good + hash_a = bytes.fromhex( hash_a_hex ) + hash_b = bytes.fromhex( hash_b_hex ) list_of_service_keys_to_content_updates = [] @@ -3399,12 +3449,12 @@ class HydrusResourceClientAPIRestrictedManageFileRelationshipsSetRelationships( list_of_service_keys_to_content_updates.append( service_keys_to_content_updates ) - rows.append( ( duplicate_type, hash_a, hash_b, list_of_service_keys_to_content_updates ) ) + database_write_rows.append( ( duplicate_type, hash_a, hash_b, list_of_service_keys_to_content_updates ) ) - if len( rows ) > 0: + if len( database_write_rows ) > 0: - HG.client_controller.WriteSynchronous( 'duplicate_pair_status', rows ) + HG.client_controller.WriteSynchronous( 'duplicate_pair_status', database_write_rows ) response_context = HydrusServerResources.ResponseContext( 200 ) @@ -3451,38 +3501,9 @@ class HydrusResourceClientAPIRestrictedManagePagesAddFiles( HydrusResourceClient page_key = request.parsed_request_args.GetValue( 'page_key', bytes ) - if 'hash' in request.parsed_request_args: - - hashes = [ request.parsed_request_args.GetValue( 'hash', bytes ) ] - - CheckHashLength( hashes ) - - media_results = HG.client_controller.Read( 'media_results', hashes, sorted = True ) - - elif 'hashes' in request.parsed_request_args: - - hashes = request.parsed_request_args.GetValue( 'hashes', list, expected_list_type = bytes ) - - CheckHashLength( hashes ) - - media_results = HG.client_controller.Read( 'media_results', hashes, sorted = True ) - - elif 'file_id' in request.parsed_request_args: - - hash_ids = [ request.parsed_request_args.GetValue( 'file_id', int ) ] - - media_results = HG.client_controller.Read( 'media_results_from_ids', hash_ids, sorted = True ) - - elif 'file_ids' in request.parsed_request_args: - - hash_ids = request.parsed_request_args.GetValue( 'file_ids', list, expected_list_type = int ) - - media_results = HG.client_controller.Read( 'media_results_from_ids', hash_ids, sorted = True ) - - else: - - raise HydrusExceptions.BadRequestException( 'You need hashes or hash_ids for this request!' ) - + hashes = ParseHashes( request ) + + media_results = HG.client_controller.Read( 'media_results', hashes, sorted = True ) try: diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index 1ed42c97..439f935a 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -84,8 +84,8 @@ options = {} # Misc NETWORK_VERSION = 20 -SOFTWARE_VERSION = 513 -CLIENT_API_VERSION = 40 +SOFTWARE_VERSION = 514 +CLIENT_API_VERSION = 41 SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 ) diff --git a/hydrus/core/HydrusData.py b/hydrus/core/HydrusData.py index 34abf895..54449b62 100644 --- a/hydrus/core/HydrusData.py +++ b/hydrus/core/HydrusData.py @@ -606,6 +606,11 @@ def DebugPrint( debug_info ): def DedupeList( xs: typing.Iterable ): + if isinstance( xs, set ): + + return list( xs ) + + xs_seen = set() xs_return = [] @@ -1381,7 +1386,7 @@ def RestartProcess(): # exe is python's exe, me is the script - args = [ sys.executable ] + sys.argv + args = [ exe ] + sys.argv else: @@ -1398,6 +1403,7 @@ def RestartProcess(): os.execv( exe, args ) + def SampleSetByGettingFirst( s: set, n ): # sampling from a big set can be slow, so if we don't care about super random, let's just rip off the front and let __hash__ be our random diff --git a/hydrus/core/HydrusText.py b/hydrus/core/HydrusText.py index 084cfac1..78a26c4b 100644 --- a/hydrus/core/HydrusText.py +++ b/hydrus/core/HydrusText.py @@ -18,6 +18,7 @@ re_one_or_more_whitespace = re.compile( r'\s+' ) # this does \t and friends too # want to keep the 'leading space' part here, despite tag.strip() elsewhere, in case of some crazy '- test' tag re_leading_garbage = re.compile( r'^(-|system:)+' ) re_leading_single_colon = re.compile( '^:(?!:)' ) +re_leading_double_colon = re.compile( '^::(?!:)' ) re_leading_byte_order_mark = re.compile( '^\ufeff' ) # unicode .txt files prepend with this, wew HYDRUS_NOTE_NEWLINE = '\n' diff --git a/hydrus/core/networking/HydrusNetworkVariableHandling.py b/hydrus/core/networking/HydrusNetworkVariableHandling.py index 52956f11..00c3d546 100644 --- a/hydrus/core/networking/HydrusNetworkVariableHandling.py +++ b/hydrus/core/networking/HydrusNetworkVariableHandling.py @@ -2,6 +2,7 @@ import collections import json import os import traceback +import typing import urllib CBOR_AVAILABLE = False @@ -300,7 +301,7 @@ def ParseNetworkBytesToParsedHydrusArgs( network_bytes ): return args -def ParseTwistedRequestGETArgs( requests_args, int_params, byte_params, string_params, json_params, json_byte_list_params ): +def ParseTwistedRequestGETArgs( requests_args: dict, int_params, byte_params, string_params, json_params, json_byte_list_params ): args = ParsedRequestArguments() @@ -311,9 +312,7 @@ def ParseTwistedRequestGETArgs( requests_args, int_params, byte_params, string_p raise HydrusExceptions.NotAcceptable( 'Sorry, this service does not support CBOR!' ) - for name_bytes in requests_args: - - values_bytes = requests_args[ name_bytes ] + for ( name_bytes, values_bytes ) in requests_args.items(): try: @@ -413,6 +412,84 @@ def ParseTwistedRequestGETArgs( requests_args, int_params, byte_params, string_p return args + +variable_type_to_text_lookup = collections.defaultdict( lambda: 'unknown!' ) + +variable_type_to_text_lookup[ int ] = 'integer' +variable_type_to_text_lookup[ str ] = 'string' +variable_type_to_text_lookup[ bytes ] = 'hex-encoded bytestring' +variable_type_to_text_lookup[ bool ] = 'boolean' +variable_type_to_text_lookup[ list ] = 'list' +variable_type_to_text_lookup[ dict ] = 'object/dict' + +def GetValueFromDict( dictionary: dict, key, expected_type, expected_list_type = None, expected_dict_types = None, default_value = None, none_on_missing = False ): + + # not None because in JSON sometimes people put 'null' to mean 'did not enter this optional parameter' + if key in dictionary and dictionary[ key ] is not None: + + value = dictionary[ key ] + + TestVariableType( key, value, expected_type, expected_list_type = expected_list_type, expected_dict_types = expected_dict_types ) + + return value + + else: + + if default_value is None and not none_on_missing: + + raise HydrusExceptions.BadRequestException( 'The required parameter "{}" was missing!'.format( key ) ) + + else: + + return default_value + + + + +def TestVariableType( name: str, value: typing.Any, expected_type: type, expected_list_type = None, expected_dict_types = None, allowed_values = None ): + + if not isinstance( value, expected_type ): + + type_error_text = variable_type_to_text_lookup[ expected_type ] + + raise HydrusExceptions.BadRequestException( 'The parameter "{}", with value "{}", was not the expected type: {}!'.format( name, value, type_error_text ) ) + + + if allowed_values is not None and value not in allowed_values: + + raise HydrusExceptions.BadRequestException( 'The parameter "{}", with value "{}", was not in the allowed values: {}!'.format( name, value, allowed_values ) ) + + + if expected_type is list and expected_list_type is not None: + + for item in value: + + if not isinstance( item, expected_list_type ): + + raise HydrusExceptions.BadRequestException( 'The list parameter "{}" held an item, "{}" that was {} and not the expected type: {}!'.format( name, item, type( item ), variable_type_to_text_lookup[ expected_list_type ] ) ) + + + + + if expected_type is dict and expected_dict_types is not None: + + ( expected_key_type, expected_value_type ) = expected_dict_types + + for ( dict_key, dict_value ) in value.items(): + + if not isinstance( dict_key, expected_key_type ): + + raise HydrusExceptions.BadRequestException( 'The Object parameter "{}" held a key, "{}" that was {} and not the expected type: {}!'.format( name, dict_key, type( dict_key ), variable_type_to_text_lookup[ expected_key_type ] ) ) + + + if not isinstance( dict_value, expected_value_type ): + + raise HydrusExceptions.BadRequestException( 'The Object parameter "{}" held a value, "{}" that was {} and not the expected type: {}!'.format( name, dict_value, type( dict_value ), variable_type_to_text_lookup[ expected_value_type ] ) ) + + + + + class ParsedRequestArguments( dict ): def __missing__( self, key ): @@ -422,75 +499,6 @@ class ParsedRequestArguments( dict ): def GetValue( self, key, expected_type, expected_list_type = None, expected_dict_types = None, default_value = None, none_on_missing = False ): - # not None because in JSON sometimes people put 'null' to mean 'did not enter this optional parameter' - if key in self and self[ key ] is not None: - - value = self[ key ] - - error_text_lookup = collections.defaultdict( lambda: 'unknown!' ) - - error_text_lookup[ int ] = 'integer' - error_text_lookup[ str ] = 'string' - error_text_lookup[ bytes ] = 'hex-encoded bytestring' - error_text_lookup[ bool ] = 'boolean' - error_text_lookup[ list ] = 'list' - error_text_lookup[ dict ] = 'object/dict' - - if not isinstance( value, expected_type ): - - if expected_type in error_text_lookup: - - type_error_text = error_text_lookup[ expected_type ] - - else: - - type_error_text = 'unknown!' - - - raise HydrusExceptions.BadRequestException( 'The parameter "{}" was not the expected type: {}!'.format( key, type_error_text ) ) - - - if expected_type is list and expected_list_type is not None: - - for item in value: - - if not isinstance( item, expected_list_type ): - - raise HydrusExceptions.BadRequestException( 'The list parameter "{}" held an item, "{}" that was {} and not the expected type: {}!'.format( key, item, type( item ), error_text_lookup[ expected_list_type ] ) ) - - - - - if expected_type is dict and expected_dict_types is not None: - - ( expected_key_type, expected_value_type ) = expected_dict_types - - for ( dict_key, dict_value ) in value.items(): - - if not isinstance( dict_key, expected_key_type ): - - raise HydrusExceptions.BadRequestException( 'The Object parameter "{}" held a key, "{}" that was {} and not the expected type: {}!'.format( key, dict_key, type( dict_key ), error_text_lookup[ expected_key_type ] ) ) - - - if not isinstance( dict_value, expected_value_type ): - - raise HydrusExceptions.BadRequestException( 'The Object parameter "{}" held a value, "{}" that was {} and not the expected type: {}!'.format( key, dict_value, type( dict_value ), error_text_lookup[ expected_value_type ] ) ) - - - - - return value - - else: - - if default_value is None and not none_on_missing: - - raise HydrusExceptions.BadRequestException( 'The required parameter "{}" was missing!'.format( key ) ) - - else: - - return default_value - - + return GetValueFromDict( self, key, expected_type, expected_list_type = expected_list_type, expected_dict_types = expected_dict_types, default_value = default_value, none_on_missing = none_on_missing ) diff --git a/hydrus/test/TestClientAPI.py b/hydrus/test/TestClientAPI.py index 118170a0..017c718c 100644 --- a/hydrus/test/TestClientAPI.py +++ b/hydrus/test/TestClientAPI.py @@ -452,7 +452,7 @@ class TestClientAPI( unittest.TestCase ): path = '/add_tags/add_tags' - body_dict = { 'Hydrus-Client-API-Access-Key' : 'abcd', 'hash' : hash_hex, 'service_names_to_tags' : { 'my tags' : [ 'test', 'test2' ] } } + body_dict = { 'Hydrus-Client-API-Access-Key' : 'abcd', 'hash' : hash_hex, 'service_keys_to_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : [ 'test', 'test2' ] } } body = json.dumps( body_dict ) @@ -464,7 +464,7 @@ class TestClientAPI( unittest.TestCase ): self.assertEqual( response.status, 403 ) - body_dict = { 'Hydrus-Client-API-Session-Key' : 'abcd', 'hash' : hash_hex, 'service_names_to_tags' : { 'my tags' : [ 'test', 'test2' ] } } + body_dict = { 'Hydrus-Client-API-Session-Key' : 'abcd', 'hash' : hash_hex, 'service_keys_to_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : [ 'test', 'test2' ] } } body = json.dumps( body_dict ) @@ -505,7 +505,7 @@ class TestClientAPI( unittest.TestCase ): path = '/add_tags/add_tags' - body_dict = { 'Hydrus-Client-API-Access-Key' : access_key_hex, 'hash' : hash_hex, 'service_names_to_tags' : { 'my tags' : [ 'test', 'test2' ] } } + body_dict = { 'Hydrus-Client-API-Access-Key' : access_key_hex, 'hash' : hash_hex, 'service_keys_to_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : [ 'test', 'test2' ] } } body = json.dumps( body_dict ) @@ -521,7 +521,7 @@ class TestClientAPI( unittest.TestCase ): HG.test_controller.ClearWrites( 'content_updates' ) - body_dict = { 'Hydrus-Client-API-Session-Key' : session_key_hex, 'hash' : hash_hex, 'service_names_to_tags' : { 'my tags' : [ 'test', 'test2' ] } } + body_dict = { 'Hydrus-Client-API-Session-Key' : session_key_hex, 'hash' : hash_hex, 'service_keys_to_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : [ 'test', 'test2' ] } } body = json.dumps( body_dict ) @@ -728,6 +728,15 @@ class TestClientAPI( unittest.TestCase ): ] } + get_service_expected_result = { + 'service' : { + 'name' : 'repository updates', + 'service_key' : '7265706f7369746f72792075706461746573', + 'type': 20, + 'type_pretty': 'local update file domain' + } + } + for api_permissions in should_work.union( should_break ): access_key_hex = api_permissions.GetAccessKey().hex() @@ -759,6 +768,55 @@ class TestClientAPI( unittest.TestCase ): self.assertEqual( response.status, 403 ) + # + + path = '/get_service?service_name=repository%20updates' + + connection.request( 'GET', path, headers = headers ) + + response = connection.getresponse() + + data = response.read() + + if api_permissions in should_work: + + text = str( data, 'utf-8' ) + + self.assertEqual( response.status, 200 ) + + d = json.loads( text ) + + self.assertEqual( d, get_service_expected_result ) + + else: + + self.assertEqual( response.status, 403 ) + + + path = '/get_service?service_key={}'.format( CC.LOCAL_UPDATE_SERVICE_KEY.hex() ) + + connection.request( 'GET', path, headers = headers ) + + response = connection.getresponse() + + data = response.read() + + if api_permissions in should_work: + + text = str( data, 'utf-8' ) + + self.assertEqual( response.status, 200 ) + + d = json.loads( text ) + + self.assertEqual( d, get_service_expected_result ) + + else: + + self.assertEqual( response.status, 403 ) + + + def _test_add_files_add_file( self, connection, set_up_permissions ): @@ -966,7 +1024,9 @@ class TestClientAPI( unittest.TestCase ): path = '/add_files/delete_files' - body_dict = { 'hashes' : [ h.hex() for h in hashes ], 'file_service_name' : 'not existing service' } + not_existing_service_hex = os.urandom( 32 ).hex() + + body_dict = { 'hashes' : [ h.hex() for h in hashes ], 'file_service_key' : not_existing_service_hex } body = json.dumps( body_dict ) @@ -980,7 +1040,7 @@ class TestClientAPI( unittest.TestCase ): text = str( data, 'utf-8' ) - self.assertIn( 'not existing service', text ) # error message should be complaining about it + self.assertIn( not_existing_service_hex, text ) # error message should be complaining about it # @@ -1036,7 +1096,7 @@ class TestClientAPI( unittest.TestCase ): path = '/add_files/undelete_files' - body_dict = { 'hashes' : [ h.hex() for h in hashes ], 'file_service_name' : 'not existing service' } + body_dict = { 'hashes' : [ h.hex() for h in hashes ], 'file_service_key' : not_existing_service_hex } body = json.dumps( body_dict ) @@ -1050,7 +1110,7 @@ class TestClientAPI( unittest.TestCase ): text = str( data, 'utf-8' ) - self.assertIn( 'not existing service', text ) # error message should be complaining about it + self.assertIn( not_existing_service_hex, text ) # error message should be complaining about it # @@ -1482,7 +1542,7 @@ class TestClientAPI( unittest.TestCase ): path = '/add_tags/add_tags' - body_dict = { 'service_names_to_tags' : { 'my tags' : [ 'test' ] } } + body_dict = { 'service_keys_to_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : [ 'test' ] } } body = json.dumps( body_dict ) @@ -1498,7 +1558,9 @@ class TestClientAPI( unittest.TestCase ): path = '/add_tags/add_tags' - body_dict = { 'hash' : hash_hex, 'service_names_to_tags' : { 'bad tag service' : [ 'test' ] } } + not_existing_service_key_hex = os.urandom( 32 ).hex() + + body_dict = { 'hash' : hash_hex, 'service_keys_to_tags' : { not_existing_service_key_hex : [ 'test' ] } } body = json.dumps( body_dict ) @@ -1510,13 +1572,17 @@ class TestClientAPI( unittest.TestCase ): self.assertEqual( response.status, 400 ) + text = str( data, 'utf-8' ) + + self.assertIn( not_existing_service_key_hex, text ) # test it complains about the key in the error + # add tags to local HG.test_controller.ClearWrites( 'content_updates' ) path = '/add_tags/add_tags' - body_dict = { 'hash' : hash_hex, 'service_names_to_tags' : { 'my tags' : [ 'test', 'test2' ] } } + body_dict = { 'hash' : hash_hex, 'service_keys_to_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : [ 'test', 'test2' ] } } body = json.dumps( body_dict ) @@ -1542,7 +1608,7 @@ class TestClientAPI( unittest.TestCase ): path = '/add_tags/add_tags' - body_dict = { 'hash' : hash_hex, 'service_names_to_actions_to_tags' : { 'my tags' : { str( HC.CONTENT_UPDATE_ADD ) : [ 'test_add', 'test_add2' ], str( HC.CONTENT_UPDATE_DELETE ) : [ 'test_delete', 'test_delete2' ] } } } + body_dict = { 'hash' : hash_hex, 'service_keys_to_actions_to_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : { str( HC.CONTENT_UPDATE_ADD ) : [ 'test_add', 'test_add2' ], str( HC.CONTENT_UPDATE_DELETE ) : [ 'test_delete', 'test_delete2' ] } } } body = json.dumps( body_dict ) @@ -1573,7 +1639,7 @@ class TestClientAPI( unittest.TestCase ): path = '/add_tags/add_tags' - body_dict = { 'hash' : hash_hex, 'service_names_to_tags' : { 'example tag repo' : [ 'test', 'test2' ] } } + body_dict = { 'hash' : hash_hex, 'service_keys_to_tags' : { HG.test_controller.example_tag_repo_service_key.hex() : [ 'test', 'test2' ] } } body = json.dumps( body_dict ) @@ -1599,7 +1665,7 @@ class TestClientAPI( unittest.TestCase ): path = '/add_tags/add_tags' - body_dict = { 'hash' : hash_hex, 'service_names_to_actions_to_tags' : { 'example tag repo' : { str( HC.CONTENT_UPDATE_PEND ) : [ 'test_add', 'test_add2' ], str( HC.CONTENT_UPDATE_PETITION ) : [ [ 'test_delete', 'muh reason' ], 'test_delete2' ] } } } + body_dict = { 'hash' : hash_hex, 'service_keys_to_actions_to_tags' : { HG.test_controller.example_tag_repo_service_key.hex() : { str( HC.CONTENT_UPDATE_PEND ) : [ 'test_add', 'test_add2' ], str( HC.CONTENT_UPDATE_PETITION ) : [ [ 'test_delete', 'muh reason' ], 'test_delete2' ] } } } body = json.dumps( body_dict ) @@ -1630,7 +1696,7 @@ class TestClientAPI( unittest.TestCase ): path = '/add_tags/add_tags' - body_dict = { 'hashes' : [ hash_hex, hash2_hex ], 'service_names_to_tags' : { 'my tags' : [ 'test', 'test2' ] } } + body_dict = { 'hashes' : [ hash_hex, hash2_hex ], 'service_keys_to_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : [ 'test', 'test2' ] } } body = json.dumps( body_dict ) @@ -2080,7 +2146,7 @@ class TestClientAPI( unittest.TestCase ): HG.test_controller.ClearWrites( 'import_url_test' ) - request_dict = { 'url' : url, 'destination_page_name' : 'muh /tv/', 'show_destination_page' : True, 'filterable_tags' : [ 'filename:yo' ], 'service_names_to_additional_tags' : { 'my tags' : [ '/tv/ thread' ] } } + request_dict = { 'url' : url, 'destination_page_name' : 'muh /tv/', 'show_destination_page' : True, 'filterable_tags' : [ 'filename:yo' ], 'service_keys_to_additional_tags' : { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY.hex() : [ '/tv/ thread' ] } } request_body = json.dumps( request_dict ) @@ -2570,7 +2636,7 @@ class TestClientAPI( unittest.TestCase ): ( location_context, hashes ) = args self.assertEqual( location_context, default_location_context ) - self.assertEqual( hashes, { file_relationships_hash } ) + self.assertEqual( set( hashes ), { file_relationships_hash } ) # search files failed tag permission @@ -2850,13 +2916,30 @@ class TestClientAPI( unittest.TestCase ): path = '/manage_file_relationships/set_file_relationships' - test_pair_rows = [ - [ 4, "b54d09218e0d6efc964b78b070620a1fa19c7e069672b4c6313cee2c9b0623f2", "bbaa9876dab238dcf5799bfd8319ed0bab805e844f45cf0de33f40697b11a845", False, False, True ], - [ 4, "22667427eaa221e2bd7ef405e1d2983846c863d40b2999ce8d1bf5f0c18f5fb2", "65d228adfa722f3cd0363853a191898abe8bf92d9a514c6c7f3c89cfed0bf423", False, False, True ], - [ 2, "0480513ffec391b77ad8c4e57fe80e5b710adfa3cb6af19b02a0bd7920f2d3ec", "5fab162576617b5c3fc8caabea53ce3ab1a3c8e0a16c16ae7b4e4a21eab168a7", False, False, False ] - ] - - request_dict = { 'pair_rows' : test_pair_rows } + request_dict = { + "relationships" : [ + { + "hash_a" : "b54d09218e0d6efc964b78b070620a1fa19c7e069672b4c6313cee2c9b0623f2", + "hash_b" : "bbaa9876dab238dcf5799bfd8319ed0bab805e844f45cf0de33f40697b11a845", + "relationship" : 4, + "do_default_content_merge" : False, + "delete_b" : True + }, + { + "hash_a" : "22667427eaa221e2bd7ef405e1d2983846c863d40b2999ce8d1bf5f0c18f5fb2", + "hash_b" : "65d228adfa722f3cd0363853a191898abe8bf92d9a514c6c7f3c89cfed0bf423", + "relationship" : 4, + "do_default_content_merge" : False, + "delete_b" : True + }, + { + "hash_a" : "0480513ffec391b77ad8c4e57fe80e5b710adfa3cb6af19b02a0bd7920f2d3ec", + "hash_b" : "5fab162576617b5c3fc8caabea53ce3ab1a3c8e0a16c16ae7b4e4a21eab168a7", + "relationship" : 2, + "do_default_content_merge" : False + } + ] + } request_body = json.dumps( request_dict ) @@ -2888,7 +2971,7 @@ class TestClientAPI( unittest.TestCase ): - expected_written_rows = [ ( duplicate_type, bytes.fromhex( hash_a_hex ), bytes.fromhex( hash_b_hex ), delete_thing( hash_b_hex, delete_second ) ) for ( duplicate_type, hash_a_hex, hash_b_hex, merge, delete_first, delete_second ) in test_pair_rows ] + expected_written_rows = [ ( r_dict[ 'relationship' ], bytes.fromhex( r_dict[ 'hash_a' ] ), bytes.fromhex( r_dict[ 'hash_b' ] ), delete_thing( r_dict[ 'hash_b' ], 'delete_b' in r_dict and r_dict[ 'delete_b' ] ) ) for r_dict in request_dict[ 'relationships' ] ] self.assertEqual( written_rows, expected_written_rows ) @@ -3294,11 +3377,11 @@ class TestClientAPI( unittest.TestCase ): tags = [ 'kino', 'green' ] - path = '/get_files/search_files?tags={}&file_sort_type={}&file_sort_asc={}&file_service_name={}'.format( + path = '/get_files/search_files?tags={}&file_sort_type={}&file_sort_asc={}&file_service_key={}'.format( urllib.parse.quote( json.dumps( tags ) ), CC.SORT_FILES_BY_FRAMERATE, 'true', - 'trash' + CC.TRASH_SERVICE_KEY.hex() ) connection.request( 'GET', path, headers = headers ) @@ -3340,12 +3423,12 @@ class TestClientAPI( unittest.TestCase ): tags = [ 'kino', 'green' ] - path = '/get_files/search_files?tags={}&file_sort_type={}&file_sort_asc={}&file_service_key={}&tag_service_name={}'.format( + path = '/get_files/search_files?tags={}&file_sort_type={}&file_sort_asc={}&file_service_key={}&tag_service_key={}'.format( urllib.parse.quote( json.dumps( tags ) ), CC.SORT_FILES_BY_FRAMERATE, 'true', CC.TRASH_SERVICE_KEY.hex(), - 'all%20known%20tags' + CC.COMBINED_TAG_SERVICE_KEY.hex() ) connection.request( 'GET', path, headers = headers ) @@ -3449,18 +3532,6 @@ class TestClientAPI( unittest.TestCase ): self.assertEqual( predicates, [] ) - # - - pretend_request = PretendRequest() - - pretend_request.parsed_request_args = { 'system_inbox' : True } - pretend_request.client_api_permissions = set_up_permissions[ 'search_green_files' ] - - with self.assertRaises( HydrusExceptions.InsufficientCredentialsException ): - - ClientLocalServerResources.ParseClientAPISearchPredicates( pretend_request ) - - # pretend_request = PretendRequest() @@ -3517,38 +3588,6 @@ class TestClientAPI( unittest.TestCase ): pretend_request = PretendRequest() - pretend_request.parsed_request_args = { 'tags' : [ 'green' ], 'system_inbox' : True } - pretend_request.client_api_permissions = set_up_permissions[ 'search_green_files' ] - - predicates = ClientLocalServerResources.ParseClientAPISearchPredicates( pretend_request ) - - expected_predicates = [] - - expected_predicates.append( ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_TAG, value = 'green' ) ) - expected_predicates.append( ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_INBOX ) ) - - self.assertEqual( set( predicates ), set( expected_predicates ) ) - - # - - pretend_request = PretendRequest() - - pretend_request.parsed_request_args = { 'tags' : [ 'green' ], 'system_archive' : True } - pretend_request.client_api_permissions = set_up_permissions[ 'search_green_files' ] - - predicates = ClientLocalServerResources.ParseClientAPISearchPredicates( pretend_request ) - - expected_predicates = [] - - expected_predicates.append( ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_TAG, value = 'green' ) ) - expected_predicates.append( ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_ARCHIVE ) ) - - self.assertEqual( set( predicates ), set( expected_predicates ) ) - - # - - pretend_request = PretendRequest() - pretend_request.parsed_request_args = { 'tags' : [ 'green', 'system:archive' ] } pretend_request.client_api_permissions = set_up_permissions[ 'search_green_files' ] @@ -3676,12 +3715,17 @@ class TestClientAPI( unittest.TestCase ): headers = { 'Hydrus-Client-API-Access-Key' : access_key_hex } - file_ids_to_hashes = { 1 : bytes.fromhex( 'a' * 64 ), 2 : bytes.fromhex( 'b' * 64 ), 3 : bytes.fromhex( 'c' * 64 ) } + file_ids_to_hashes = { i : os.urandom( 32 ) for i in range( 20 ) } metadata = [] for ( file_id, hash ) in file_ids_to_hashes.items(): + if file_id == 0 or file_id >= 4: + + continue + + metadata_row = { 'file_id' : file_id, 'hash' : hash.hex() } metadata.append( metadata_row ) @@ -3709,6 +3753,11 @@ class TestClientAPI( unittest.TestCase ): for ( file_id, hash ) in file_ids_to_hashes.items(): + if file_id == 0 or file_id >= 4: + + continue + + size = random.randint( 8192, 20 * 1048576 ) mime = random.choice( [ HC.IMAGE_JPEG, HC.VIDEO_WEBM, HC.APPLICATION_PDF ] ) width = random.randint( 200, 4096 ) @@ -3888,56 +3937,6 @@ class TestClientAPI( unittest.TestCase ): metadata_row[ 'tags' ] = tags_dict - # old stuff start - - api_service_keys_to_statuses_to_tags = {} - - service_keys_to_statuses_to_tags = tags_manager.GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_STORAGE ) - - for ( service_key, statuses_to_tags ) in service_keys_to_statuses_to_tags.items(): - - if service_key not in service_keys_to_names: - - service_keys_to_names[ service_key ] = services_manager.GetName( service_key ) - - - s = { str( status ) : sorted( tags, key = HydrusTags.ConvertTagToSortable ) for ( status, tags ) in statuses_to_tags.items() if len( tags ) > 0 } - - if len( s ) > 0: - - service_name = service_keys_to_names[ service_key ] - - api_service_keys_to_statuses_to_tags[ service_key.hex() ] = s - - - - metadata_row[ 'service_keys_to_statuses_to_tags' ] = api_service_keys_to_statuses_to_tags - - service_keys_to_statuses_to_display_tags = {} - - service_keys_to_statuses_to_tags = tags_manager.GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_ACTUAL ) - - for ( service_key, statuses_to_tags ) in service_keys_to_statuses_to_tags.items(): - - if service_key not in service_keys_to_names: - - service_keys_to_names[ service_key ] = services_manager.GetName( service_key ) - - - s = { str( status ) : sorted( tags, key = HydrusTags.ConvertTagToSortable ) for ( status, tags ) in statuses_to_tags.items() if len( tags ) > 0 } - - if len( s ) > 0: - - service_name = service_keys_to_names[ service_key ] - - service_keys_to_statuses_to_display_tags[ service_key.hex() ] = s - - - - metadata_row[ 'service_keys_to_statuses_to_display_tags' ] = service_keys_to_statuses_to_display_tags - - # old stuff end - metadata.append( metadata_row ) detailed_known_urls_metadata_row = dict( metadata_row ) @@ -3971,6 +3970,8 @@ class TestClientAPI( unittest.TestCase ): # fail on non-permitted files + HG.test_controller.SetRead( 'hash_ids_to_hashes', { k : v for ( k, v ) in file_ids_to_hashes.items() if k in [ 1, 2, 3, 7 ] } ) + path = '/get_files/file_metadata?file_ids={}&only_return_identifiers=true'.format( urllib.parse.quote( json.dumps( [ 1, 2, 3, 7 ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -3995,6 +3996,8 @@ class TestClientAPI( unittest.TestCase ): # identifiers from file_ids + HG.test_controller.SetRead( 'hash_ids_to_hashes', { k : v for ( k, v ) in file_ids_to_hashes.items() if k in [ 1, 2, 3 ] } ) + path = '/get_files/file_metadata?file_ids={}&only_return_identifiers=true'.format( urllib.parse.quote( json.dumps( [ 1, 2, 3 ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -4013,6 +4016,8 @@ class TestClientAPI( unittest.TestCase ): # basic metadata from file_ids + HG.test_controller.SetRead( 'hash_ids_to_hashes', { k : v for ( k, v ) in file_ids_to_hashes.items() if k in [ 1, 2, 3 ] } ) + path = '/get_files/file_metadata?file_ids={}&only_return_basic_information=true'.format( urllib.parse.quote( json.dumps( [ 1, 2, 3 ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -4031,6 +4036,8 @@ class TestClientAPI( unittest.TestCase ): # metadata from file_ids + HG.test_controller.SetRead( 'hash_ids_to_hashes', { k : v for ( k, v ) in file_ids_to_hashes.items() if k in [ 1, 2, 3 ] } ) + path = '/get_files/file_metadata?file_ids={}'.format( urllib.parse.quote( json.dumps( [ 1, 2, 3 ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -4084,7 +4091,7 @@ class TestClientAPI( unittest.TestCase ): # identifiers from hashes - path = '/get_files/file_metadata?hashes={}&only_return_identifiers=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) ) + path = '/get_files/file_metadata?hashes={}&only_return_identifiers=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for ( k, hash ) in file_ids_to_hashes.items() if k in [ 1, 2, 3 ] ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -4102,7 +4109,7 @@ class TestClientAPI( unittest.TestCase ): # basic metadata from hashes - path = '/get_files/file_metadata?hashes={}&only_return_basic_information=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) ) + path = '/get_files/file_metadata?hashes={}&only_return_basic_information=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for ( k, hash ) in file_ids_to_hashes.items() if k in [ 1, 2, 3 ] ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -4120,7 +4127,7 @@ class TestClientAPI( unittest.TestCase ): # metadata from hashes - path = '/get_files/file_metadata?hashes={}'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) ) + path = '/get_files/file_metadata?hashes={}'.format( urllib.parse.quote( json.dumps( [ hash.hex() for ( k, hash ) in file_ids_to_hashes.items() if k in [ 1, 2, 3 ] ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -4150,7 +4157,7 @@ class TestClientAPI( unittest.TestCase ): # metadata from hashes with detailed url info - path = '/get_files/file_metadata?hashes={}&detailed_url_information=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) ) + path = '/get_files/file_metadata?hashes={}&detailed_url_information=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for ( k, hash ) in file_ids_to_hashes.items() if k in [ 1, 2, 3 ] ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -4168,7 +4175,7 @@ class TestClientAPI( unittest.TestCase ): # metadata from hashes with notes info - path = '/get_files/file_metadata?hashes={}&include_notes=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) ) + path = '/get_files/file_metadata?hashes={}&include_notes=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for ( k, hash ) in file_ids_to_hashes.items() if k in [ 1, 2, 3 ] ] ) ) ) connection.request( 'GET', path, headers = headers ) @@ -4186,7 +4193,7 @@ class TestClientAPI( unittest.TestCase ): # failure on missing file_ids - HG.test_controller.SetRead( 'media_results_from_ids', HydrusExceptions.DataMissing( 'test missing' ) ) + HG.test_controller.SetRead( 'hash_ids_to_hashes', HydrusExceptions.DataMissing( 'test missing' ) ) api_permissions = set_up_permissions[ 'everything' ] diff --git a/hydrus/test/TestClientDBDuplicates.py b/hydrus/test/TestClientDBDuplicates.py index 4c245974..531b16d2 100644 --- a/hydrus/test/TestClientDBDuplicates.py +++ b/hydrus/test/TestClientDBDuplicates.py @@ -749,7 +749,9 @@ class TestClientDBDuplicates( unittest.TestCase ): self.assertEqual( len( file_duplicate_types_to_counts ), 5 ) - self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], self._get_group_potential_count( file_duplicate_types_to_counts ) ) + expected = self._get_group_potential_count( file_duplicate_types_to_counts ) + + self.assertIn( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], ( expected, expected - 1 ) ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_MEMBER ], len( self._our_main_dupe_group_hashes ) - 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_FALSE_POSITIVE ], 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_ALTERNATE ], 1 ) @@ -765,7 +767,9 @@ class TestClientDBDuplicates( unittest.TestCase ): self.assertEqual( len( file_duplicate_types_to_counts ), 5 ) - self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], self._get_group_potential_count( file_duplicate_types_to_counts ) ) + expected = self._get_group_potential_count( file_duplicate_types_to_counts ) + + self.assertIn( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], ( expected, expected - 1 ) ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_MEMBER ], len( self._our_alt_dupe_group_hashes ) - 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_FALSE_POSITIVE ], 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_ALTERNATE ], 1 ) @@ -810,7 +814,9 @@ class TestClientDBDuplicates( unittest.TestCase ): self.assertEqual( len( file_duplicate_types_to_counts ), 5 ) - self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], self._get_group_potential_count( file_duplicate_types_to_counts ) ) + expected = self._get_group_potential_count( file_duplicate_types_to_counts ) + + self.assertIn( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], ( expected, expected - 1 ) ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_MEMBER ], len( self._our_main_dupe_group_hashes ) - 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_FALSE_POSITIVE ], 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_ALTERNATE ], 1 ) @@ -824,7 +830,9 @@ class TestClientDBDuplicates( unittest.TestCase ): self.assertEqual( len( file_duplicate_types_to_counts ), 3 ) - self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], self._get_group_potential_count( file_duplicate_types_to_counts ) ) + expected = self._get_group_potential_count( file_duplicate_types_to_counts ) + + self.assertIn( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], ( expected, expected - 1 ) ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_MEMBER ], len( self._our_fp_dupe_group_hashes ) - 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_FALSE_POSITIVE ], 2 ) @@ -869,7 +877,9 @@ class TestClientDBDuplicates( unittest.TestCase ): self.assertEqual( len( file_duplicate_types_to_counts ), 5 ) - self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], self._get_group_potential_count( file_duplicate_types_to_counts ) ) + expected = self._get_group_potential_count( file_duplicate_types_to_counts ) + + self.assertIn( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], ( expected, expected - 1 ) ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_MEMBER ], len( self._our_main_dupe_group_hashes ) - 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_FALSE_POSITIVE ], 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_ALTERNATE ], 1 ) @@ -883,7 +893,9 @@ class TestClientDBDuplicates( unittest.TestCase ): self.assertEqual( len( file_duplicate_types_to_counts ), 5 ) - self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], self._get_group_potential_count( file_duplicate_types_to_counts ) ) + expected = self._get_group_potential_count( file_duplicate_types_to_counts ) + + self.assertIn( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], ( expected, expected - 1 ) ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_MEMBER ], len( self._our_alt_dupe_group_hashes ) - 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_FALSE_POSITIVE ], 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_ALTERNATE ], 1 ) @@ -906,7 +918,9 @@ class TestClientDBDuplicates( unittest.TestCase ): self.assertEqual( len( file_duplicate_types_to_counts ), 5 ) - self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], self._get_group_potential_count( file_duplicate_types_to_counts ) ) + expected = self._get_group_potential_count( file_duplicate_types_to_counts ) + + self.assertIn( file_duplicate_types_to_counts[ HC.DUPLICATE_POTENTIAL ], ( expected, expected - 1 ) ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_MEMBER ], len( self._our_main_dupe_group_hashes ) - 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_FALSE_POSITIVE ], 1 ) self.assertEqual( file_duplicate_types_to_counts[ HC.DUPLICATE_ALTERNATE ], 1 ) diff --git a/hydrus/test/TestClientTags.py b/hydrus/test/TestClientTags.py index 0727bb12..130776f0 100644 --- a/hydrus/test/TestClientTags.py +++ b/hydrus/test/TestClientTags.py @@ -461,9 +461,9 @@ class TestTagObjects( unittest.TestCase ): self.assertEqual( pat.IsAcceptableForFileSearches(), values[0] ) self.assertEqual( pat.IsAcceptableForTagSearches(), values[1] ) self.assertEqual( pat.IsEmpty(), values[2] ) - self.assertEqual( pat.IsExplicitWildcard(), values[3] ) + self.assertEqual( pat.IsExplicitWildcard( True ), values[3] ) self.assertEqual( pat.IsNamespaceSearch(), values[4] ) - self.assertEqual( pat.IsTagSearch(), values[5] ) + self.assertEqual( pat.IsTagSearch( True ), values[5] ) self.assertEqual( pat.inclusive, values[6] ) @@ -475,8 +475,8 @@ class TestTagObjects( unittest.TestCase ): def read_predicate_tests( pat: ClientSearch.ParsedAutocompleteText, values ): - self.assertEqual( pat.GetImmediateFileSearchPredicate(), values[0] ) - self.assertEqual( pat.GetNonTagFileSearchPredicates(), values[1] ) + self.assertEqual( pat.GetImmediateFileSearchPredicate( True ), values[0] ) + self.assertEqual( pat.GetNonTagFileSearchPredicates( True ), values[1] ) def write_predicate_tests( pat: ClientSearch.ParsedAutocompleteText, values ): diff --git a/mkdocs.yml b/mkdocs.yml index 317de3fe..c8d96f14 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,6 +27,7 @@ nav: - Advanced: - advanced_siblings.md - advanced_parents.md + - advanced_sidecars.md - advanced_multiple_local_file_services.md - advanced.md - reducing_lag.md diff --git a/static/default/gugs/twitter collection lookup.png b/static/default/gugs/twitter collection lookup.png deleted file mode 100644 index c8b0328c9dc5e43b069ca9f5302c5d995a85981e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2396 zcmV-i38VIjP)@6`!fo(xxO1D0LvBcO@>a8lW*$q;^q4iBS+~!dgk7whBm~5VV&ne8hhwf^{0F z;SbmYDMGP)?mwwg-8F3x*0r_@+>s^%yC0NuQziJ+sN4z#&I!t1M8SdHEjzP2do%lS zPA;kL$iAPFo7tK7-i&8{`{vEw-5f?=e+mJ@3v9p`gc>)&5C8!HKmb5ku>gi3G&75JvoG&*lUFC?9z1mvi6& zLg@ki{fVg+f1kbkjaP{>%8?7yo1p-{d z0O!NhY{Ss_UBJ|-_H|cKJ&)LX-VY$adxdV4@hvyR>L9V}-H)*CiXaGtAP@iqXl#Jq zgK-SclB+z9Tct5O%C@VW)I@gB6n~zD*|RY|80YG;ag1?%+NViQsCwZvz=C^TpM>+N zUL5lc>cxh9nvLO9bw|K9Bw3V=;oelK@eD8;3spr(R@F3`Bv(nRKQkz87&L2FJE>8W zk(^9ONY)J^l9sr?z^p@RS|$ZW2}x>NK`3Duwki8nFi_QS8nC5Y`e{W;npbJA=YE22 zwT$_yprn(pb|tOQUP#3TkH67q0@y=SL@LQk3YwujD6pa8MvkU39ZqvpRazz`IZHB9 z$gC1M$+B0F=$!Ec5Dah{tN}~Pt&ehMk`QheKS8%zhRwf+0JP&pTOJ5NhK}C(lXT={ z$p|1o2FZryBM>cD4UDAdvuq0Zv0CfJa}U_qUe-qM%1DV{$oZ_z}SBMXuo< z*l3m@un*0WyZyVnfg@p9knb;~Qs(b7p#`>y&eY`#^5L#+I(xz*XSBu@|JaCf^4( zi3czvEImUyBPGbcAP*o1-Sf0A?aAtH4B;`l^uoagIE?^P9{rkvUbU{|@Sx>)koBO0DgKGR zzA}&bP{#U0|*8<4W}U{CF%!x z08!9SO|f-h z6xV3m)hyM?gEpk5buR%7JxOLaNApBXN)`+^0R#hrhSdOTR@4t9fGFsvRS8)daLop->xn2x7A-o6Fr7$bhS~M*kop=#@`qlVx2%QZvR#Djs)??5u*g!T_uDet}lKs;}A7gCjEbwWZs{quj(xKx%BabG*vbASTBx zI>^W9v5h#M!f245wpbeUHF80!Vb!ss4dPhdW!7mFqXA4@y18u3I(75l(ErW3(@ji( z6LQ4KpL)#5^jE92d$t-|d(6z)y9~5tf)q_`@fav+T55P-u+^%S7kBAv?{S%R8YOoz z(xNMMy|mq_^MD-=S{n5n`caBY2$q04jxJE`!!)X{l`R2^Y+uuJ`RFk`l6MI(Ar;&{ zoUYF<*M0aj$ab!7)u#g0_uQ7U3;NGZ`M?tcf9+Dv32O~ z8uGHi4E3|yyarTr#xB;dDp+atm=ziwFsIf!C~%Fl;lR}&4ar9DJ517DMqy2-7>O1N zg?-0i38({o`^j8Ih{v9ewtdAA=axJ3Z_QH(J;&zdEHBn}aHNJ+(TbC8bzmJj%Cep6x(I);wlct9wYE=Bf65;J#IQz#M!PD~o(;At zTJaPJjVc3V2N}@QZ2#oOntw_O2v7&wa@z-|HI2-md{QeN<*L4h%z#{TE;s%5*cX|H zmv$1JZpFUdcCciHgPP{C>ayZ4a2l0QmdvVDsq~A0VD2DVZrLBz*aILw64j>n00HX3 zDt+ZhGd*FYNu;f?t39*aJWBcx1`vjx2ro*$-_T{Z_m7_Kwd#^V4tsxc{}0?8!v# zlKS)$pZ$LK(|_6U+WlYZemipevD@A}lKjX^%NOMz{_v4cw;g!%^ia!po_Jt={sZeT ztpDr2_TkrmczB>!m0xfD@~xL=4xib4=$${0pWpGX>5=#wKYA(t>Xt{frJJAZ`OWXT zE|xx6eH{Gq{Y@!}8u`RL(?R@}9J@rb74&IjNB-4iF)KM%8Yo$)`$#Z;`4^TvDt O0000$&dhxo?88ksg$lpA`TAC|Y0V9sod2Az;P) ztNSqT#R32`39X}P5;(PGiT5}^%C8=SbTN%15E-`mJqR^HU_g?Q6Q*#Ul`$c%_U5}*U*D7nRt<2$ zc8;5N)yiCQu*J;|Vg~|t+s@)U78pq@wMQODLTnXm%$MT!!v(U}7X{hkE1>Ll=ZEVP z)Q>#PEdU4uto}il9V-L#MS590(=KHd#RP+cvTkY6oag*s*Lxo_G=cD??%Cw#Z}V^C ztp(EoPMALDE)5rx7Lw_%*609H)qraQ_Djz>O6!kif}$e;&9A^jy>SmO8>0tJ1O|g& ze{DNYdto+w)aL~bGsb0u`q;ZBFt}teimavAfH9v+`@;y}k1FrH9_?!) z&}mB&kk9q4o-ktJvdsIghYcjLEq)+>hb1*kt=J2TbT^y*#cZF@Sy=V(1F=?gp~YD0 z>{|i5pzv$$yD}!+!se#SF0pQyu+rQsUVL{H;lnQpNY;qM@E;jYA&ADTX2sl%{K4&{I z^oCtmsuz2uUv|kEpP$BM)069Ti0w^F&ngGoG}P>Dk;8{|v3{*-VmG~u-FVnLk8hzM zk3xmocqU?9f12f(Nrt|`^*N8XN}ztxVNK!A+0NzBbf;^w_x^|5G8JcnB z82{If8mYQl_@>^vLVRPw?1{y<7xnlwu_-P2B~HnbC)QO%%h;Q{z_l2McjHJrPYR+? zIuIyMFKDdg-$<3X3CuQ1Y%8LNpnVQ4HD3fCxs#>7=ZW;GKAB`AGg0PR7(M6E6$b=$%mPj@p^>4l&r2P{W3dE0YJ@mwfEf6ESwy0S zWCzemKcV~c$CG}H$;ILlOOJfzP>mRBy_z7z!LSYE)|sDIz%7$X9y=$3@tx@X%g?}w z7@X5cKEg*YrfG%OddsIM3WTBQTA|V2^uNCW02lxUIqYLgAn=zhoK658@Y^B&5BTjS zI6wnIm`!?CH%c@v+OsI-)pZ3)C#&n~=sD#jo5V^w%&E<`l?>`ENJqgj!*>0fV zD6zDlD7S2nTh^rq%`Z{D#fwMNV&%Ip$JBE=H`q0^6*g+5A)(DD+O(vqm)0C<)s9w< zSwG@RMH^Bikor9(Hh#UlE~6J(ocb6olRh5m2H}K~tCG{ei4Mzyk*l4SeDP&)kqd3yrQgcYwiN3SsY}X zk}a33>j?eCVMFh-)DmO=^2z48ra;?={&=9Hh!Q8>`T1!fNxAHHqDu6~h405N9)#n; z+Sfx|W<~A-UD|J+8DWau4f4IpmmlRRbw=t|zs`}sO#WURDb!N(dseqcNXpsg5H>t} z3!}mc!Yf#*;%@@-aWheKBYFM;f@gn4eWM;P#ICW_w`INA~Y8 z5B$vkaGuEO)!WOvLL6$d5>B6v=lSE9IIMvgBC=2UxhcW2`Ade=cHBKKCWwBWA-|tp zT6mK{ejFH%7NiZbZf%I-4pJ1K*D{T7<&BcO5xf6o{24j|x%cOzj>vbZ|MCXIG7da7 zna%Xyx1!X-8pF`dmo5cYK}nyjK>OtEml7vuo= zgxqO}{|a^x0O0=%ZG!)qMjiZ9?Cg%mS_NzlO_#L}KeE|Z$F@*?!uKMa^>|jxnwD4Z zv($d-c8-g3K5UzR^Mz90oqRK}54wEl7t_{;?>7C|x-Y1AjJx|&_OO3BxlZY1w+D*4 z`*nV_c`Gx(D{M8;x$3ZWoIi$!Y#V6_Ss}Ga;)_y7gzgKu6}QwaQnO@2nod0P{YHQ0 zk0;I3<_-r6)JAT`5&8)9v}b! diff --git a/static/default/gugs/twitter list lookup.png b/static/default/gugs/twitter list lookup.png deleted file mode 100644 index 6377215a992c4bacaae0c1ed5cce30c903c61ab6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2305 zcmai$c|6k(1INFZpV8(jlpJ%6Tq))F2^&(VC`8KKt&(FBa~sK(azxItA}W*PF}ImH zqKuexj!6d=IcMbhJ-_Go{QJC~e?G7G=k@vf^Lc#|uUcJ%3P=h7006ZxKW_s7-2Xsv-qE4lHXF|kHY?Zv!o$Gla=(@FGQ zBfVtD%<3AHAo^MQwlyJc};! zmlfm^1)@PnJ+b>3u+*bV$E#TxQIw?I<4fki9XC^H1tjhIZ8gmS9zE!|_Y36v&EQ|X z0_t(b(F&diT=Fx}Z(!rY727V#eUxWdG&rwaKKP zx0XNax&-U)P&1k8ulO*JuWOX54n|5Bs8;=gN(UsVGYC_bwE6pgH#DcWD^cuhaT+HZ{&wI$1VqfzyN1t`HoO z8*&Q)&rD!+JWl!PuSPWb)i_cA-!@VlH@`|jm2(_yNs5j|%9l$@vD zHM`lBR7~VWf~2d>kZEZ9v1G0nhay*E#=x}bx=FiGXmk8AA;JfScSfqdq_F97;gkY- z-hHW#@57T^e3WngyB4F*g$R1g&`kY|ANkwk8CxBT$C|>dx`~#hrt#sRASAfafjXnA z#tn#=IRc1t;)>ZtX5e(95i?)Y3|wV0`x`LncJB$6kn4W-9DN84H(t_Ip~PO9{hiLR zCS&1DW=x3-TcIfijGV)fX~xOc(I-y8!NOc=mjar_&h}UV>%|gsND%0H(y0Me|GiZG z10C)+dz}tlG}5aWN+gT3P-dE6{1Z!P&0u8I>Ycs!z`7^%cJbt#u`PkUl7Mmt3&ke6 zn8-%?wZSmsRnJ9SwZMkakY1a&AS;Rt^<@tUzOi}fE3M#eNzE}ID0Up4losuZh!oyf zeZ{YYE#>yV*%#!bIjBgkgEA7<-U(!T~53m z=Jmk(3$KdmHaagTNco7!H!cuh zrwuKPxz*Kp#&BIVZkg(z-VYG77Wfl=)|Cct^Al7%%f;2vIdQ_4szDZ{MuPq`-bVGr z)o=*KQ>9w#5AmoYjf*Z$NKg1)HP8y}@Z%9N2SEP}z-e?R3JCqL`%f|c3H~3}OTZYi zR5?)gmyiadBW7{Xca=v(3XTQb?zGQBvg|Y$zUNT2O2MnIhsYsu0?zrC*O!GI0%8(| z2JZ8v6ZXR%S;C}oYWUi@8l&dnFYyJc6)RRdbzhuxcS4qg6;Y;-Dv;^d4;md?z`>a# zb{g-tb1?odyNih5D{ffRco{E2B_BoYv%+yBQFSTfU1QI`_%8|2WBh%^QXYtDa(!7< zxFGQ2n5>)1F=%gCg7U}kuxwn)o$a2jB*_Mqj4H1iX%QV~TwF)yGjLg!c8?*Y$+fff zKq1!{k$+Cm4YHE9qFgntM-IQ52>F)x?T69piaZ&oDI#S$AiSQ#fOvZTDWzx6@S6Tu z7Eedfc+pR-#!-+5?VAtv&)lsUryNcQbyQL@d}4RQC0wh0IA5(e$yQ>E6ni{GwoH>N z{Y{SnR@Qo0VJr&}kaL`GzhbXpKloUz#4NsGmapJS#~}KGO?Rn=ou*z1Jk8TVqzz8s z22YZdLk!^Y@w8{0^Tl7*GXOV0wjHbNS_>;qDgUsQRK z+{-EhZ+2i=)8w#OJ&#n-P0xKUUMcqBE%iMYjI>rg_wy z-X77mNwBxw65?cXiA|9_Q+V-|{Ix$%$AaE~G!~YMpitYxz(rjhNDg1?JwaCYLUS;? zM26AyXs@3hAm2z2ly>)0mOSBYe~5U*{wRJw_-iO*qw6<2FQ!xTq14%(_pTl&)Wj2_ z@69Ip5%ygNv8YC}s*DgPyn;u#tdD0>TXYn&kH3uDo=cfIZ=ZC1h1s5n0ZZfBJ%HGM zdHjzWbqu;9yaeEpm41Y#fcfug_!OPJ`2|WZK5)fq%C9H4t|Xng^}*VC`yMTAxem1R z<7X*(gY;fk$cl6^PhWhqwf6Sh;=};sPSbAb{`N2S)uleB;o9PEUXNP8VMK)IgtH^L z#b=$lL)QQ1b#DDFCqg@5htU7T& SKTH4n+gO-coquoQ8uJ$^(N22+ diff --git a/static/default/gugs/twitter profile lookup (with replies).png b/static/default/gugs/twitter profile lookup (with replies).png index 8a25eb2f3e678007ca2bcd79f27639c8d98ccf9e..663b943b76f112f9f6ccf4a86ac4ab8818126447 100644 GIT binary patch delta 2792 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 literal 2773 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
        $3# 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 N002ovPDHLkV1jXYJre)` diff --git a/static/default/parsers/danbooru file page parser - get webm ugoira.png b/static/default/parsers/danbooru file page parser - get webm ugoira.png index ecabe7885c14540755ceca5cd34251ba2425ae27..bac3157c6783f02a209d1bd28791bd17a47708c7 100644 GIT binary patch delta 3296 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 diff --git a/static/default/parsers/pixiv file page api parser.png b/static/default/parsers/pixiv file page api parser.png index 5940568ffb7e11ee9101d51f3472ad43aa959ddd..f5e850865311f5f46413e8666537e68639618950 100644 GIT binary patch delta 3024 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 SoJ4p40000l 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 diff --git a/static/default/parsers/twitter syndication api timeline-profile parser.png b/static/default/parsers/twitter syndication api timeline-profile parser.png new file mode 100644 index 0000000000000000000000000000000000000000..d53f955cc280148a85cdf2ee3a0f5edf01660222 GIT binary patch literal 3421 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 literal 0 HcmV?d00001 diff --git a/static/default/parsers/twitter syndication api tweet parser.png b/static/default/parsers/twitter syndication api tweet parser.png index dac8ea7b1e5ab383e4de6a74e67be03d694e43a1..7db74aad4fa1ded26679ebba2057b0580b2f245c 100644 GIT binary patch literal 3480 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 ZkdP+@N3oGwWB+-%R4}TF74q2N{{d`kcYXi> literal 3446 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 diff --git a/static/default/url_classes/twitter list.png b/static/default/url_classes/twitter list.png deleted file mode 100644 index 95e2823617a020552e5e30eb4e1291501fe61a4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1906 zcma)7`8V5%9{ond5;8G%sztn_MKD^#)=;}jGj`fZ?UbREQfdjR)GjHp3#u4X%c!xH zrnI58G*qc7DY5UnsAP=3f8f1eKIe1pIrp5;J@?-8d5p!F!Jv{*003YX=EgPvz;U_) zIG!Ifh-Wt$0Ju>W#)fwHzpNL>VuiiLdrS8PQqeT7h9XxxQ@L30ve_p&UWj+fLYVlJ z!8~Z%pZmKC>r`rO~5xfJ1QD!t&;~+ zzvXriZC5h&!b(4=9j+aF(2FP8)4O_`)9ZYYiDS^ONa55)^e{ig7SDmk0?9VKuWiZV zlI2s~=Qk0i_dK9bw6k%qAgH>of%l~eoP6nPpcbU0VX!3OEEo^bf}$h%BbRa)-7*#S zvWGAOAYt>RHmi>ck$T|5>sNB-sLuGEtvAZ`hy!Md6C4B1$%DL6fQHAIU70ZjDGI!2 zl)Y4eqJyDi<^v!WZr7*?K)N5DFM;Ghhps`vMG(bo-J|+D0pWK#Z;m}aht59ttv+~P zu3lGg#Wbv(8|w+BgqeEg>+R|Wf)hEg<=o`Y$d-OanC%EW4hy_Dd>eZbas$?K^{F5f zZTMB7>qLV8)y~~kb)#hHeRpBl5C<0EfdXw0sK#Z$Tqx?s37!2N{!1ZbR4ct;B5C0` z^mvmS4bpQZydL$}U!9byzCmVh!~*=FQoxygtxiP9iysgm0J-B z9%tvZhBV8OR_-CcU3@p+o`XLu$pdrBO7?-hh0}&{=`)t;SuF)ifK7DadAO);l)Y5$J)KmW0y_qCKLZ0DAD)gR8j)2Df@H7CZ2twbxVMqETXT6WO`B1X$6~b z^qDAA0#1UK9Z-A7^Os=;3e8_^X3#>llWAhQ=pfOr{8We*YluEZf2PmD^mRT~lEF_Q z_pO6tybnYu|ahQF`Sr-kT!m7&_(p$Kt8%APHD28WAE;3Ki3eu;>>*;(} z+j_2eSZAk~%bY`^^x=?d=dL9OFUC3?A|J=bU!dKrV66G)Xfl}iPH)PLL8f?{y_9ND!rLJz_6MFcZ5@Uk-6>OrWEg3>PuMD0MJnRJy@ zVB-G_RXbdLG)9pHeCvdG4*A>7vaZuSy{dq_(L*U>I2A~A=|bS@pIG8;T;gsChk#Cg zv{@D(vjUr#K<6q41cZV39|icYu`-GPCj?Lae?i(E@>W4V7MP86om(M5r0=eFDnX>a zhc?Zb3C!^1~dWK3?rvcbcf=W26& zexjKk+uQZnY`U|#QZCB6#aXtPQ%Q`h1$!r6Pm411*6~OUagKN}Y0#luWvf=%4bhou zt>k1YN_l4KAKc^_Rg+i^vuhW(wCsb<@V5f(q?EfN!>0@l&LEn@m@4Pq?+N2vYTcqA zSN*O^cG$}88T8k~+z^eoiNVQ8#w^g9e5E{l zjZ=avQclD`8rA{ZlONyceRRPloRdk`0~kb37l~LnncIUm7%Al8TC(&AaH zMB?_4d^EQyTtIl~g{DD^P-<`mt17N(hU|Eo;O#B7DTr7UJ@X(9uF>aCjHmk^xEPI! zQ>t7Andq|R9MQt(I?gJ&q>WJCW%8iBWp|P7sGq-xBiLi*cO-(R6TGHRC873uHAz8A z%X^$|E3PvOd?=AwN^TTC$nfD5<@~%KagJ#%sqRReF17Eyr^bhw8Un6HwdFU75VoZW zYZM=%`+eV;xysvaZ{sAuNWnx`p8pk||K!PL+UT)Y2<<6wgLEXjX1#GoDRDi&5%%k1 zs&($(qrwCK&95vshxLtmE0`DT!V!J#%nr&J)t`jH;2c;~W_4oH;R~teOo=O(q8bfi z{U+<$pM>*~iaM~(-jUZFjRx}5w~tkuqnu7WB5?C8VsmAWx^9E{{>UUXDI*x diff --git a/static/default/url_classes/twitter syndication api collection.png b/static/default/url_classes/twitter syndication api collection.png deleted file mode 100644 index b08d4f7a180556834bd51f5940bbb693ab5f5d26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2592 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 F{SQ(VUNHaw diff --git a/static/default/url_classes/twitter syndication api likes.png b/static/default/url_classes/twitter syndication api likes.png deleted file mode 100644 index 417f9ee1d88175264594566af2169ac7529b1dd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2610 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 diff --git a/static/default/url_classes/twitter syndication api list (screen_name and slug).png b/static/default/url_classes/twitter syndication api list (screen_name and slug).png deleted file mode 100644 index 8546e0c38e16838f86b3c2c9485585a3625d75a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3394 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 diff --git a/static/default/url_classes/twitter syndication api list (user_id and slug).png b/static/default/url_classes/twitter syndication api list (user_id and slug).png deleted file mode 100644 index bff93d3dd1166be73003afa74a45710732b84cab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3009 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 diff --git a/static/default/url_classes/twitter syndication api profile (user_id).png b/static/default/url_classes/twitter syndication api profile (user_id).png deleted file mode 100644 index 4412799f08efda00c95b63e10033e72e4ce97cad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2921 zcma);SH`I*7aC{M;3#@R(0vaz)(sDu3Sg*-uXB?GBH zyV$-y1~i`~8AAJD&}j_@=vO}S+G9077&&Hu!Sq}~_N72+tO8h`oY+to)s-ecYk#`c z$5I8fl7O?<6NjDHiOad6^9(kCvk8dAuiNvg{h+Nm79xxjlEdXs#eR^S{p(X0I}|!C zG|$_Q^)p&tgpXW=kqsJ*^3En>>{!$tA~O=s=kq$#H3Px`0DU;xa7`!8y2|~j8Ko(8 z&HGB`W!wOT$=8pXgAa7p+Z}X94t*q4YnS%A)JaD_ClQG&^vX^dX2a3TCWJ_?<)@-@ zt9t$jYf7DU!8S9ueQU+9>PgZl1BFh<$huGiIYP4tcc`u8bQl+~LTmf$qeM%7dSpQM z=}+SpJ*|F3h_W~1nize*Ny!(1cg%w9D_KFZl`mY6ZIzU~4DWGwajr?}BpYt$drP_% zI#%$(kXBkLW=btE-3uyjKyi8=6Rs*>ucf-9CkRM{R6>^8g|2xkR(tLVkzw0bxa$?s z&mfhq=S@Yu*H!16x=qU7ghQ%2kR^iZBWZH!mdIl_tuROu@*TpKwJ<4)J3rM+Upv6C za1dRZeE9{a<#X8}PPjhkd)%HBrCp({z#1%t5hoM_gHH>dD=2O=W@kw#CST&bGBYoH zrR=LYDHAFt%P7IObis!@)Qu5WkjA@pL8gnm_*IW8YRY{kj6F*WxTc{l)B;`KasNlD z5aWft3m}^Xw^v;m!9M`JjZuIx7;#U*UXdb3iN|P*maBK+*}t)WCEbW-K~cA@F!R~N@8UBUzoE=k|&EfMoGZQ8E+!oQuuwzbQX^v>c)Mh2Tc}8s~2{=-w9_5{Fds`6g(#&W?W`+9Sqjkge%AL8P8S36C6 z%wTA^BV@_g%tqfIH>uO2_!`AjJMRG2G#Vx>gu`$TK1#?C^Z0=_vglfcqBE2ImEop2ib1##(5d1!~_tur! zR;W)^`=8hr20%-szNa2Nz&zVCTxb8*7}5WxXE)~(d)*dSNm#~EsyjU0$S!%ZhZ`3r z@(edjJeM-i80xC8d?}6^qq?MU5Kzu)BAOZEbHdIfHxa|@ z=~2y4vis%Y&xBluZJTrRMPoQ}*MS}5bXH#_q)gwV*TL$KbzU;5Mq%L+NoXxawD59# zy2D5JL66d3@7xVp%aF!}?16958_w6{!CrSGtK~jdR+oW#{iO?AqdZg&3yB>Q?zK4YYT<*i0 zXshD98{x9#qaZh3;%2>BWn$Q-cSaj%0|EvC|4tVG0f7GpaZSA7$(<-eAYcxq{Zqpl zmir*kso3kC+}+V8yt;O&m^LX5dJ|WAPum3nt@G8wtv=(7mCeZ^h4>9E%e)r7FCVyc znjWp={tQ&Eq!QFuJFlY`bEz1iH_#nbC|`by{JR)dt$R-GDrKW$VLghOI*A$RCc55! zcsAg0Yf5@Q;2@ZV<#$IwJU{c!546lHW!uM0W5;vk85No_inf7es52OR`>X1E?#sy! zmOM^llM0obv>FvcxN1MMgN>!B2lH;!hYh7zvx8jQZWCr*tzcGASX;bTg$P+wKY^~E z-kD+&(K{jbk*na2PCZsz$|W*4hKmEmlJ&vzfT~>?t3j3TE3*QN)21hrQ zpP2_KD_rDe<`$MQvlLKS3|YE4B zwFLWTsrAR{zaIGlOG3P$^6OvZS2aI*-?@lBKeDfT%xZeqr+1Gw<~A=Z%h$ABOQw(H zxD0<<&BZlEVbs|7g3-}*12!M2-emgUg6z6MZ_yAvrxJOZXDh$&Zuy>NNowx3z)SGh z$!v{g%t6Mt4*br_ExE9o8A+`3zW>(YC(7psYloPnRs4`fMLd+|va$P{eQQy~UEVl! z@1MF9C;pRP0WpiMN6P~;?)C;Tms_?zwWs$-JQ+=FRH5O=8jj0-D_Qtst>wJKnz>SQ z1kHHz4oBzO9@+cZsbcm28sC>NWyOZJr!oG>J0*p>jMnsY zqgm6uIdzTEb%hbt?9||{?3af!JJ_vgpEFP1Ugv)#r_D2g@u7IW#uT_6_o+ t3&KO;4<8yWcW%Y2%?WmT_jWg(VfoKkQLUj7Z~k0p0EsZstI%Y{;`LPa$8p5h&#Ul8Uj`+^1yjpn8 zyBKA-fX;$`vJbrS$F;`@uc(}#RW7f{dkeAZQpGB))|6trJ>9b-JnW4{Id0_B)=d^m z?QL@n4Ja1uTaVLosF(Xv`_B1k+fMM#_CnmNo55scXW)VG2fLE{dEih0-~~D=I6zOA zxgp%Q992WoWsZ?TN&x0r5Fek?g<=^$A)IQL`=7nWS1h*DP@g|IK5$PMaPjr_1Ap23 z&=ihy>_tc=Sy1#3)KWGQkTfT11~}n>-`04-2U!FPR^B6cTs) zD*M5!zcn=<-Jn^Pr_vs zk_Z)(?1-{@;Mw>3cY&%eDS~RZnAZ9L6M#)Q@>yaRywX%kCb;R`D$Shqa6pXw%8v*- zxmb^lEsxhe2?ZXgFa}NuUs7#NaaA}piT$xu$B?0Z{E=KruL?U$T5LVU1a6$I(2d}}FJz); zg!cH`4O^N9>Q2^XHEWJ%i--bkAkyrm;UZ}<;t)wM!Jn0A@SKqRYq=rmR9bq_Kr>#n z4>9N#q59?1R;{yxT@x6f-m^edzxNBQBoNh z&^-2ZTRbIGp(!`zad~yxgpMTAk){Jmnfu9`FyyjN5Ki5a;L7o2FJeU%oiJ9m|}16rSLNK^OGh;WRL9w{Va4+Z|<~3U8+5lhC0({(9F6`!ihM-PXRu zj@p3pWn5GHF!P7}va@NsYX!bpTfK~w#Qr|Y6F++7y){QT44j(#d$J^wZZzCK$6*hM z(%%_=xUsHN@i^MRK6jtmB_NR_Olk^WT|v3aCRufTd$@?Kn^62qAt$k04ZdMF+0VsN zgSHirM5M_=jD|#B&pvC8qS6#%X;l%EMZ|%MIm4V%pX?(glqONomL+Oojl19$?yu8G zqX2@rD7wTUHly;dy|7sge~Mf)T%Bv00W83}v~C&=bI*zY{__$`JC*J)-ak)*dsk z=O74y$tRHzrXMRQ^8(J+#}tz1krRN6ZKPAV!5lLF>H10x z&$mN&_|j@tm;?ip&n!+jZ}4$5RR^?o#P?!AJk5O}hFPV-^rLBQ8>Y6wX`53bG&y63 zRiGZvpBqeTV4V8!OB5ATR<7!tl8dC%olV&^hA6_8g?whqGX+HnT_a??lHiLqK)s;` zf0L4Th3*l%k6UFGwWAQVvL9oKv!|x$>|abpGblw%N+!6i;lLfO@_=m0Sc4#CrnSHv z3L4!}KJw&{$9P&gZoi3%28CH4T~G7K+T;uk;Cg4G^T1iGKXV6N+SmGMzg}koeAG!8 z>t)lTIfq&s3g{kG1NacKD?PAGLdr0Yy(xabjfPhO32v-vXXDC&x9*Hp?A`H~S%ROg zyDr}&>H6iY+me$e?;xJh1nY!S8B+-O%3$|k!u^;9G?utkA+|=;bWD+FsihEaK47#4?m^kw`J})`49j?vlV;DAD6~xnFLNd|;BZIo!%gDn6aM{_caH7CV<2GOF z;@HHZ)$r`Iey6p?OL{^=L49wf#d{d&(GK=wLfN57!R=9XI?D_v`FqCFwsHBHG4oEW zd;@8Vu5eK@0WVr!qLfY3o6zwZqYR?_@vA@@qZ`aC%q3{B^K{*gK&4o5w=&gLAS;*U zuA=4?I9zOYT&X@{MWb14`Px5J4+071mQI=JLwjRfF!;J#eX>qQ)q)hcG=!2tJpAfh zIKr5F%E-w3y`P7Q!}&>_^gl$t@sMpfJ9g5HZ^u2AfcYkKQ_|$ov~<2rN_1%K6|q?o z&5>Bu4dIJhE?NR@_S;v+<+yZ0uP9JgKUrW-s9!Afyj-;i#L7l@l0&1=Qr_w<;lm#w zv44JdyH#@c7$!zD(PMiwUB*|7}ETP?2*4rEBJqJ#+rB$YRYHZk`!=I zGS}DBcjKglwF{HZfv|@Sm$+Y(e{MSR88KR)j5 x8r`0e-)@ng^py(o=Jm_pU1&flDs-VP+c6xYIR8TA@xSk&mii;Lauw_F{{Sx`(jx!> diff --git a/static/default/url_classes/twitter syndication api timeline-profile.png b/static/default/url_classes/twitter syndication api timeline-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..de6a17ed497c7bdcf59b296efe57a8c7c28cd064 GIT binary patch literal 2807 zcma);`8(8&8pgka>@;I7vWt|ZcQj0hkv&Ux!q`LEvSgjfmPsl_CT1)dOCkx$GPd5z zKK5lWSwf5@#}H<$r}vyc;GAEc>+{=l-`8_r&viexv@qh~6yXE_fXBpG-x>gz{vhDW z{?EL}zMTR9oUJDMy0%eMzmReF1jfYTOzFtL@N?`24=#9ELqOFcpeNQ;ZeHmetIT3M zKLtIcN5#`atL$5M-E;RlS)F~Z#1Jmj_Df&LBXK-!H&>{2v2<4Hvz7bX>0bfKT$cj| zrz{&Nt!}Yy+|5H z#BX{`Y{vN-dPDWv#>ZL1;=!I&+Vdq+mp3MtKJq@Y#6u9Q?QQtsDq=Xv{|U5;n|ndg zeO{n$+Zn3O!@@3=X=Vtqkwj3>6bxRAH`U`jCy8MH@idoD@6;*k#4j001=E+5lb|SS zW7x{F_6(U7BAE%9)2Sc3sWA;@P>a=4075PkJdkYfh`TMUEPaX{+`(P|&l-7;3wP>% z0AXhbiwg`6#3`>*=jYZ1!IJv$ui7UJjs4u=AEdqj+xS!tn7bgr0sx>&{OzZFyM=1O z);%$!`ZxIzcQ%rUfg3rA^l~bGCciRmQN5lz5^Eb6=>Xb!*1oMtYhbRPK?LItbs93B z|LTf*;|q((0zRhyY#Wo*;pJ9@D0_c7gEzNy~GxC7(^533jwR{ZdYE`63lzW?85F-iZhHR$nexE zO}SJXDWrO}?`So=kYPR)Qo(=L>KUXz#nT4zckQKO>9Lg6b)3;OEntl}XDcD$u{G&vh5 ztW>gf@u?jXd!(_06Vp9M)Uv6QVTRtE4nrr{ySIVECK1V*8dhB*{1jj%8#W;P3{_F|@<0k3AOB%^WiN_{UrdIrfv z1v*=_GHXNTZsctBw4Sz1Vmce3e6(fMir%Fe$o{lPC zTD&hZmRH9`B8S>U+pP;(%Xf25CO{B7D-)>N4NsQMn6qF|t=8VpyC#nfcHndTpv2Lg zzHv3|oVzvC}7&(}e;T!JyI>lm}* zql#unZ1_7~R35+B`uSG3A=p)V1tWqr=MvS$G@V*70u)r`u;u(A*MciAtnJ9elY0TC zE>CHYHwA@wac+~s#%@Wbut@F@M#KchH;JV|3@`Oh-9AFl!U%ED1aqC8DfRW1Ogz7Q ztOrLl*QlCZ{>X6X!M942y;Q10-$Q==KMN+~+hy<@GIDD>=i@tj|Jd>zK8L6YyvpFE zLnIMM-=Vfv0etSnh(u&0?UG~{UII!vS{wP?LT%clxukFm5?ymZ=6s$^wuDlG!Ty<} zXLvd6CjHJCeE+yUB68 z%lQ?;!O+^#Oxi(Sdyv6opX#fzbkS}RgR;9Ga+!D`NyN#+d}GBtd^#@gF0#)|c5oGB?beTokG(E5AADc-yLX$8I&t6MkyfRP zH!i2cAX zG;UDio^jOI=Myjcl}KVP51q3m46+M+9h|(^)f+fs%oMpnM-W!=&4K04Q!5u6n^G0o z7RJJnpZSTQmfue>MbQS0fmt647COJJXof&%H8g1qHinj}RrDEYgib1ezrtbWH^UCr ztpkJRyKW4sM`Z2><{P`q!(?Kh#*X$@xli`=eE%)sdT~mfK3&(;D}grARNqLx+F>4R$ldnyOYf8A_s%7=amS~7vu3M{QOl*J z$}QD%B}PGqyTa9Wk38MPPP#vOX*X^!NSU6f`At{Ju-&XD!S{H4b$vP+3SPY+?TlLCS#dtjJ&8r+CmUp|X3Jn{Y^Hvaz5Y+5H8HTzue|P__-}eRE}Z}X literal 0 HcmV?d00001 diff --git a/static/default/url_classes/twitter syndication api tweet-result.png b/static/default/url_classes/twitter syndication api tweet-result.png index f0431cfe514db7f81dffeedb1cde0f2f82424e11..08094398b66ada7d08f480a2d1253d3914e074ae 100644 GIT binary patch delta 2561 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 N002ovPDHLkV1g|G?ScRR diff --git a/static/default/url_classes/twitter syndication api tweet.png b/static/default/url_classes/twitter syndication api tweet.png deleted file mode 100644 index b07b3855993e73afd20f7d758b41ca2cc9c63527..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2500 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 U6Nw$Ae?Bm4b33yNH2(H~0luhxt^fc4 literal 0 HcmV?d00001 diff --git a/static/default/url_classes/twitter tweet.png b/static/default/url_classes/twitter tweet.png index 4c72cf8a9c2a92262bd9b2b610eeef9ecd98e7c1..37773efcb1ddf38dc0f79e284c6496723fb64964 100644 GIT binary patch delta 1834 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