Per code review: the album-bundle helpers (release picker + staging
collision suffix) were defined as private symbols in torrent.py and
imported by usenet.py through ``from core.download_plugins.torrent
import _pick_best_album_release, _unique_staging_path``. Sibling
plugins shouldn't reach into each other's private surface — leaky
module boundary, and the underscore prefix says don't import.
Also addressed two latent issues at the same time:
- The Auto-Import sweep race: my plugin copied audio files into
staging via plain ``shutil.copy2``, which exposes a partial file
at the audio extension for the duration of the copy. The Auto-
Import worker filters by audio extension when scanning Staging
(AUDIO_EXTENSIONS in core/auto_import_worker.py), so a mid-flight
scan could pick up a truncated file. Fix: copy to a
``.tmp.<random>`` sidecar first, then atomically rename via
``Path.replace`` (which is ``os.replace`` — atomic on the same
filesystem). Auto-Import sees the file either at its final name
or not at all.
- The 6-hour poll timeout was a hard-coded magic constant. Users
with slow private trackers or large box sets would silently time
out after 6h. Both the timeout and the poll interval are now
read from config (``download_source.album_bundle_timeout_seconds``
/ ``..._poll_interval_seconds``) with safe fallback to the
existing defaults when unset / non-numeric.
- core/download_plugins/album_bundle.py: new module owns the
shared surface — ``pick_best_album_release`` (with quality_guess
passed in as a parameter to avoid the circular import that would
result from this module trying to know about torrent.py's title
parser), ``unique_staging_path``, ``atomic_copy_to_staging``,
``copy_audio_files_atomically``, ``get_poll_interval``,
``get_poll_timeout``. Module-level size constants and quality
weights live here too. Usenet's grabs-as-popularity-proxy is
built into the picker so both plugins get the right behavior
without divergent local logic.
- core/download_plugins/torrent.py: drops the local helpers + the
hard-coded poll constants, imports from album_bundle. Per-track
download flow still uses module-level ``_POLL_TIMEOUT_SECONDS``
/ ``_POLL_INTERVAL_SECONDS`` aliases (read from config once at
import time, same as before from a per-track perspective).
- core/download_plugins/usenet.py: drops the imports of the
torrent.py private helpers; everything goes through album_bundle
now. Stops the cross-plugin private-import leak that started
this whole refactor.
- tests/test_album_bundle.py: 23 new tests covering the picker
heuristic (empty input, singleton drop, FLAC preference, grabs
fallback for usenet, size-floor / ceiling boundaries), the
collision-suffix logic, the atomic-copy invariant (concurrent
scanner thread asserts it never observes a partial audio file
during five sequential copies), the failure-skip behavior of the
batch copier, and the config-driven poll cadence including
garbage-input fallback.
- tests/test_torrent_usenet_plugins.py: existing picker tests
updated to call the new module-level helpers instead of the
former torrent.py privates.