Import pipeline: probe, hardlink, rename, layout #82
Merged
naps62-yolo
merged 3 commits from 2026-08-22 23:21:31 +01:00
issue/23-import-pipeline into main
Labels
Clear labels
area/api
area/ci
area/compat
area/core
area/daemon
area/db
area/dl
area/indexer
area/infra
area/meta
area/parse
area/probe
area/subs
area/web
difficulty/easy
difficulty/hard
difficulty/moderate
difficulty/trivial
phase/1-skeleton
phase/2-logic
phase/3-sourcing
phase/4-movies
phase/5-ui
phase/6-tv
phase/7-people
phase/8-compat
phase/9-subtitles
type/bug
type/chore
type/feature
type/test
arr-api
CI and test harness
arr-compat
arr-core
arr-daemon
arr-db
arr-dl
arr-indexer
workspace and tooling
arr-meta
arr-parse
arr-probe
arr-subs crate: providers, extraction, translation, sync
web/
bounded, obvious approach, few files
subtle correctness or cross-cutting
multiple files, judgement, an interface to design
one file, mechanical, no design decisions
workspace, CI, config, database, empty API and SPA
parsing and policy engine, pure, no network
TMDB and Prowlarr, read-only
movies end to end
search, buckets, library views, queues
seasons, episodes, tracking, derived status
owner tags and notifications
Jellyseerr shim
build order: subtitles, DESIGN.md §15
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: yolo/arr#82
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #23 (DESIGN.md §7.2–§7.4).
A
downloadedgrab is the gap: the daemon probes the torrent's files, picks the feature, re-evaluates the policy with real evidence, and hardlinks it into the library — copy only onEXDEV, staged through a rename so no partial file is ever visible. Attribute tags come fromffprobe; the source tag is the release name's claim, the one thing a file cannot know (§5.6). Hard fails blacklist the release, fail the grab and reopen the gap; the torrent is never moved or deleted (§7.3). Grab selection now skips blacklisted names (§6.3) so the next candidate wins.New:
arr_core::layout(naming, pure),arr_parse::normalise(blacklist key),TransmissionClient::torrent_content(file list),ImportActionon the reconcile lane.Acceptance test produces the exact §7.4 path and asserts the seeding file keeps a link count of two; a DV Profile 5 probe hard-fails end to end.
just cigreen.🤖 Generated with Claude Code
@@ -0,0 +139,4 @@let paths: Vec<PathBuf> = content.files.iter().map(|file| content.download_dir.join(&file.path))file.pathcomes from Transmission and can be absolute or contain..;Path::jointhen probes and hardlinks outsidedownload_dir. Reject rooted and parent components, and verify the resolved path stays below the download root before selecting a feature.@@ -0,0 +144,4 @@// No expected runtime yet: the movies table carries no TMDB runtime,// so feature selection is by size alone (largest readable video).let selection = match self.prober.select_feature(paths, None).await {select_featureprobes every torrent file serially with a 60-second per-file timeout, but the Reconcile lane cancels its whole action after 25 seconds. A slow or multi-file torrent restarts from the first path every tick and can never import; give imports a compatible budget or persist/bound the work.Fixed in
5856dbc: torrent-declared paths now go throughsafe_join, which refuses absolute paths, drive prefixes and..components before anything is probed or linked. A torrent whose every entry escapes the root hard-fails as "no readable video file". Covered byhostile_torrent_paths_never_leave_the_download_root,a_torrent_of_only_hostile_paths_hard_failsandsafe_join_refuses_escapes_and_keeps_normal_paths.Fixed in
5856dbc: probe results are now cached per path across ticks (ImportAction.probed), so a cancelled tick resumes where it stopped instead of restarting from the first file — each tick makes forward progress as long as a single probe fits the 25 s budget. Entries are dropped once the grab settles. Covered byprobe_results_are_reused_across_calls. The remaining single-probe-over-25s case is a local-disk ffprobe, which stays well under it.@@ -0,0 +120,4 @@let outcome = if let Some(outcome) = cached {outcome} else {let outcome = match self.prober.probe(path.clone()).await {This cache is populated only after
probe()completes. The reconcile timeout cancels a 25–60 second single-file probe before that insertion, so it still restarts forever; use a compatible action/probe budget or retain work that survives cancellation.Fixed in
c6f94aa: each probe now runs as atokio::spawned detached task that writes the cache itself before returning. A tick cancelled by the lane's 25 s budget drops only the await — the probe keeps running (its own 60 s limit intact,kill_on_dropno longer triggered since the task owns the child) and deposits the result for the next tick. Covered bya_cancelled_probe_still_deposits_its_result, which cancelsprobe_allmid-probe and asserts the next call reuses the deposited result with exactly one ffprobe invocation.