feat: add sorting and filtering for search results - #292
Conversation
58eff4c to
9483861
Compare
|
@stabldev would you like me to add a GIF in README to show the changes in this PR ? I use VHS script to generate it, would be a nice addition to the README file. |
I'd love that! also could you rebase the PR? |
|
The repo convention is a committed file, not an attachment URL — README line 13 uses An alternative in vhs is to upload the generated GIF into a cloud server, which will return a link that you can add to the README file to show the Demo, I do this personally to avoid uploading large media files into the repo. Can do either, what do you prefer ? Also the other 2 open PRs #293 and #294 will also create merge conflicts, I will go ahead and resolve the current ones here, and you can make the call on the order of merging those 3, which I can resolve future conflicts later. |
Let's do the first method, |
Sounds good.
For this PR yes, I will slightly change the generation script and add a new GIF. |
Search results can now be reordered and narrowed in place, without re-querying the indexer, since the full result set is already held in memory. Sorting: - Clicking a column header sorts by it. Clicking the active column reverses the direction; clicking `#` restores the indexer's ordering. `Title`, `Size` and `S:L` are all sortable this way. - `s` opens a popup listing the sort fields (relevance, seeders, size, title, leechers). Each entry shows the direction it will apply, so seeders/size/leechers default to high-to-low and title to A-Z. - `S` reverses the current direction. - Sorting is stable, so ties keep their original relevance ranking. Filtering: - `f` hides dead torrents (0 seeders). - `x` clears sorting and filters, restoring the indexer's own ordering. The key bindings only fire while the results table has focus, so typing `s` or `f` in the search box is unaffected. The results border shows the active state, e.g. `results (12/40) - seeders v`. Defaults are configurable via `general.default_sort`, `general.default_sort_order` and `general.min_seeders`. Ordering logic lives in a new `core/results.py` (`ResultView`), kept free of Textual imports so it can be unit tested directly. It sorts the underlying `Torrent` objects rather than calling `DataTable.sort()`, because that passes rendered cell text to the key function: size cells are formatted strings like "6.00 GB", which sort lexically below "850 MB" and would have to be parsed back into bytes to compare. Sorting the model keeps the exact numeric values. The sort keys coerce missing numeric fields to a sentinel rather than trusting the `int` annotations. Real trackers return null for size, seeders or leechers when they don't publish them, and comparing None against an int raises TypeError part-way through the sort, which took down the entire results view. This covers size, seeders and leechers rather than special-casing whichever field happened to fail first. Two pre-existing bugs are fixed as a consequence of routing all rendering through a single `_render_rows()` path: - Row numbers could skip. The render loop enumerated the full result list but skipped duplicate magnet URIs with `continue`, so the counter advanced for rows that were never drawn. - `_search_results_map` was never cleared between searches, so entries from previous queries accumulated and could be resolved on selection. The README and the usage guide embed a recorded demo of the sort menu, the direction toggle and the seeded-only filter, following the existing convention of committing the asset under `docs/_static/`.
9483861 to
5fcb023
Compare
|
Hey @YousefHadder! great work on this PR. When setting Another one is- currently, pressing "x" resets the view back to Relevance (raw indexer order). If a user has a custom |
|
@stabldev Good catches, both. Sort order — confirmed, and your diagnosis is right: One wrinkle on detecting "not explicitly overridden": So I'd make
The trade-off is that it should then restore I'll check tomorrow for your response here, then make the fixes and test them. Let me know if you have any other concerns, Thank you. |
|
@YousefHadder spot on! Using Your proposed approach sounds great. go ahead with the fixes whenever you're ready. |
Closes #202.
Search results can now be reordered and narrowed in place, without re-querying the indexer, since the full result set is already held in memory.
Sorting
#restores the indexer's ordering.Title,SizeandS:Lare all sortable.sopens a popup listing the sort fields, each showing the direction it will apply, so seeders/size/leechers default to high-to-low and title to A-Z.Sreverses the current direction.Filtering
fhides dead torrents (0 seeders).xclears sorting and filters together.The bindings only fire while the results table has focus, so typing
sorfin the search box is unaffected. The results border shows the active state, e.g.results (12/40) - seeders v. Defaults are configurable throughgeneral.default_sort,general.default_sort_orderandgeneral.min_seeders.On the approach
#240 suggested using
DataTable.sort(), and I want to explain why I didn't, since it's the main thing that would need re-litigating.DataTable.sort()passes rendered cell text to the key function. Size cells are formatted strings, so"6.00 GB"sorts below"850 MB"lexically, and S:L sorts as"1882:8877" < "559:851". Getting correct ordering would mean parsing those strings back into the numbers they were formatted from. It also only reorders_row_locationswithout touching cell values, so theNocolumn would shuffle out of sequence, and it can't hide rows at all, so filtering needs a separate mechanism regardless.Instead, ordering lives in a new
core/results.py(ResultView) that sorts the underlyingTorrentobjects and keeps the exact numeric values. It has no Textual imports, so it's unit-testable on its own. Happy to revisit if you'd still rather go the other way.Also from #240: no type annotations were changed, and both
TitleandS:Lare sortable by clicking the header.Two pre-existing bugs fixed along the way
Routing all rendering through a single
_render_rows()path fixed two things that were already broken:continue, advancing the counter for rows that were never drawn._search_results_mapwas never cleared between searches, so entries from old queries accumulated and could be resolved on selection.Rebased
#290 and #291 have landed, and this is now rebased onto them as a single commit containing only the sorting and filtering work. The null-handling and column-width changes that used to show up in this diff are gone.
Docs
The demo above is also in the README, below the existing one. I committed it to
docs/_static/rather than linking an attachment, to match howdemo.gifis handled and so it renders on the docs site too.docs/usage.mdgained a walkthrough of sorting and filtering,docs/configuration.mdcovers the three new keys, anddocs/roadmap.mdmarks the item done.While writing that up I also corrected four pre-existing errors in the keybinding docs:
rdoesn't exist, quit isctrl+qrather thanq,dandDwere missing entirely, andEnterwas described incorrectly. Those are unrelated to this feature, so say the word and I'll pull them into a separatedocs:PR.Testing
138 tests pass, including 4 snapshot tests and a new
tests/test_core_results.pythat coversResultViewin isolation. CI is green on Python 3.10 through 3.14.Beyond the suite, I verified against a live Jackett instance rather than fixtures — all eight sort key and direction combinations checked for correct ordering across 5,753 real results.