Repository navigation
Conversation
The torrent list re-downloaded and re-parsed every torrent each 3s tick, and pollEvery's blind timer restarted fetches slower than the interval forever - an endless spinner on large instances (#10k torrents). Polls now go through ref.polled (next tick armed after the fetch settles) and fetch only the delta since the previous response; a per-instance store keeps the merged list and survives screen exits, so re-entering paints instantly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reuses Instance.pollingIntervalSeconds (already editable for Glances) and shows the Polling Interval field on the qBittorrent instance form. List and transfer-stat polls follow it; detail views keep their fixed 10s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A refresh fired while a /sync/maindata poll was in flight (pull-to-refresh, or the invalidate after a torrent action) went out with the same rid. Two replies to one rid can land out of order, so the older one overwrote newer fields and wound the rid back until a full update healed it. QbitSyncStore now chains its fetches, so each one asks with the rid of the reply before it; a failed fetch does not block the next. The store's doc no longer claims re-entering the screen paints instantly: the autoDispose list provider still loads once, only its first fetch is a small delta now. Adds tests for the merge (full update, field patch, removal, restart) and for the chaining. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sync store is never disposed, and it kept every torrent's full raw row from qBittorrent (~60 keys incl. paths and info hashes) next to the parsed model - several KB per torrent for the rest of the session, tens of MB on a large seedbox. Merge each patch over the stored model's own JSON instead: toJson/fromJson round-trip every modelled field, so the result is identical for omitted fields, explicit nulls and extra keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The store moved rid on before merging a maindata reply, so a reply that failed to parse part way left a half-merged list marked as caught up; the server never resends what was skipped. The old per-tick full fetch healed itself on the next poll. Advance rid only once the whole reply merged, and reset it to 0 on failure so the next sync gets a full update. Not reachable against a well-behaved server (verified against qBit's per-session sync state); this restores the old self-healing. Also cover a torrent added between polls, a reply without a rid, and removing a torrent the store does not hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same interval drives the transfer-info poll, so a tick is not only a maindata delta. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list poll interval is now per instance, and the form field is the only way to change it. Pin it next to the Gluetun case so trimming the form's kind list cannot silently strand qBittorrent on the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The torrent model is now mostly rebuilt from maindata patches merged over its own JSON, so say that its keys must stay the server's. The add doc pointed at a full fetch the app no longer uses. The not-autoDispose note belongs on the provider, where that choice is made. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elta The store's provider outliving the autoDispose list is what keeps a re-entered screen from downloading every torrent again, and every other test overrides the list provider wholesale. Drive the real wiring through a container: open, leave, re-open, and expect rid 0 then 1. Fails as [0, 0] if the store provider is made autoDispose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list interval now comes from the instance. The form refuses less than 1s, but an imported profile is saved unchecked, and 0 (or less) would re-run the list and speed requests back to back, with an error backoff of 0 x 5 as well. Floor it at 1s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge test compared only name and download speed, which keep their server key as is. Comparing the whole model also covers the fields stored under a renamed snake_case key, so a toJson that stopped writing the server's keys would now fail the test (mutation-checked). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
STATUS.md still gave the old fixed 3s cadence; the list now polls only what changed, on the instance's own interval (default 5s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test file's header described only Gluetun, though it now also checks that the qBittorrent form offers the polling interval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t sends them The getMainData doc read as if a full_update reply still held only the changes; it holds the full list, which is why the store clears on it. The rid reset doc claimed the server would never resend a skipped reply, but qBittorrent resends what follows the last rid the client sent back; the reset is there because that resend would land on a half-merged list and likely fail on the same entry again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… merge test The fixture row left seeds, peers, priority, ETA, category, tags, tracker, ratio and the session counters at the model's own defaults, so a merge that dropped any of them would rebuild the default and the whole-model compare still passed. Mutation-checked: removing num_seeds from the merge base now fails the test; it passed before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…interval Only the 0 → 1s floor was pinned, so a qbitListPollInterval that ignored pollingIntervalSeconds altogether still passed every test. Mutation-checked: returning a constant 1s fails the new test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test only checks the interval helper, not that the list provider polls on it, so its name no longer claims the latter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bhavyashah94
reviewed
Oct 9, 2026
Bhavyashah94
left a comment
Collaborator
There was a problem hiding this comment.
Tested this PR against a live qBittorrent instance (v5.2.4) with ~75 torrents, as well as running on an Android emulator (Pixel 7 / API 35). Leaving a review comment with test findings for other testers and maintainers.
Summary of Testing
-
Delta Sync & Store Logic (
QbitSyncStore):- Verified initial sync behavior (
rid: 0receivesfull_update: trueand parses all torrents). - Subsequent polling ticks query
/sync/maindata?rid=$ridand only receive changed fields (e.g. download/upload speeds, progress, eta), cutting down bandwidth significantly compared to polling/torrents/info. - Verified that partial field patches correctly preserve unchanged model properties (
added_on,category,save_path,magnet_uri, etc.) across JSON merge roundtrips. - Verified sequential request queuing via
_tailFuture chaining to prevent overlapping calls (e.g. manual refresh vs timer tick) from racing or winding backrid. - Verified that if a malformed patch fails to apply,
ridresets to 0 so the next poll recovers with a clean full fetch.
- Verified initial sync behavior (
-
Polling Interval Configuration:
- Verified that the
InstanceFormScreennow renders the Polling section and Polling Interval (seconds) field when configuring a qBittorrent instance (defaulting to 5s). - Verified the interval logic enforces a 1s minimum (
max(1, instance.pollingIntervalSeconds)).
- Verified that the
-
Android Client UI (Emulator):
- Dashboard: "Active downloads" card updates live download speeds smoothly without spinner thrashing.
- Activity Tab: Real-time speed updates and download progress render reactively.
- Torrent List: Torrents render with stats, status badges, and search without full-list redraw flicker on every tick.
- Torrent Details: Detail sheets (Overview, Files, Trackers, Peers) open reliably and remain consistent with the synced state.
-
Automated Tests:
- All 73 tests in
services/service_qbittorrentpass (including the newqbit_sync_store_test.dartsuite). - Form widget test (
gluetun_form_polling_test.dart) passes. dart analyze/flutter analyzereports 0 issues.
- All 73 tests in
Everything looks solid and operates cleanly on live data. Leaving review open without approval so other testers can also test and verify on their setups.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
Every 3 seconds the torrent list downloaded every torrent from
/torrents/infoand parsed all of them again. With thousands of torrents that's slow and uses a lot of data. On top of that,pollEveryticks on a fixed timer, so when a fetch took longer than the interval, the next tick restarted it and the spinner never went away.Changes
/sync/maindata, the same endpoint qBittorrent's web UI uses. The first call returns everything. After that, it returns only changed fields, new torrents and removed hashes.QbitSyncStorekeeps the merged list per instance. It isn't autoDispose, so reopening the screen continues from the lastridinstead of fetching the full list again.ridgoes back to 0 and the next poll gets the full list.ref.polledinstead ofpollEvery.Size
Testing
flutter analyzeis clean forservice_qbittorrentandapp. All 73 tests inservice_qbittorrentpass, as well as other qBittorrent and polling tests.rid, overlapping syncs wait and use the newerrid, a failed sync doesn't block the next one, reopening resumes the delta, interval comes from the instance with a 1s floor, and the form shows the field for qBittorrent.LLM Disclosure
Developed with assistance from Claude Code (Opus 5.5). The LLM helped me identify the related files, code parts and proposed possible code modifications and fixes.
I've reviewed, cleaned, refactored and hardened the code the best I could, but my Dart capabilities are not that good.