Files
avandClaude Opus 4.8 1369a9cabe Приём: дедуп по target_missing/orphaned + стоп-кран «Закрыть»
Два дубля-близнеца на один инфохэш рождались, когда повторный приём
попадал на запись в target_missing: дедуп искал только активную задачу,
а target_missing терминален → заводилась новая загрузка, воркер усыновлял
уже присутствующий торрент и раскладывал его.

- Приём: критерий дедупа расширен до «блокирующей повторный приём» =
  активные ∪ {target_missing, orphaned}. Повторный приём такого инфохэша
  привязывается к существующей записи (спящей, без обращения к qBittorrent),
  а не плодит близнеца. Прочие терминальные (done/cancelled/failed/reverted/
  deleted) повторный приём не блокируют — осознанная свежая попытка. Новый
  read-метод FindReingestBlockingByInfohash (приоритет активной над desync);
  общий active-гард не тронут.
- Команда «Закрыть» (Dismiss) — универсальный стоп-кран из любого состояния,
  кроме deleted → cancelled (error_code=user_dismiss). Только меняет статус:
  файлы (в т.ч. хардлинки done/orphaned) и раздачу qBittorrent не трогает,
  в отличие от «Удалить». Веб — danger-зона внизу страницы; Telegram —
  кнопка с подтверждением; из cancelled — идемпотентный no-op.
- Транспорты при дедупе на desync-запись сообщают адресно (target_missing —
  привязать заново/закрыть; orphaned — закрыть и добавить заново); веб при
  дедупе ведёт на страницу существующей записи.

Спеки: ingest (дедуп), state-reconciliation (стоп-кран); граф переходов
допополнен рёбрами <терминал>→cancelled. OpenSpec change
dedup-target-missing-and-dismiss заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 20:15:37 +03:00

6.6 KiB
Raw Permalink Blame History

Why

При повторном приёме торрента, у которого уже есть запись в target_missing («разложено, но файлов в библиотеке нет»), рождается загрузка-близнец: дедуп приёма ищет только активную задачу по инфохэшу, а target_missing терминально — активной нет, заводится новая загрузка, воркер усыновляет присутствующую в qBittorrent раздачу и раскладывает её. В итоге на один торрент две записи (done + target_missing), причём у осиротевшей единственное действие «Привязать заново» упрётся в уже занятый целевой путь. Пользователю нечем аккуратно закрыть лишнюю запись, не снося при этом раздачу.

What Changes

  • Предотвращение дубля на приёме. Повторный приём инфохэша, которым владеет запись в target_missing или orphaned, SHALL привязываться к этой записи (возврат существующей, Deduplicated), а не заводить новую загрузку. Критерий дедупа расширяется с «активной» до «активной или удерживающей источник ради незакрытого намерения» (target_missing/orphaned). done из дедупа сознательно остаётся размножаемым (повторный приём завершённого = осознанное «хочу заново»). Приём остаётся быстрым: qBittorrent не трогаем, авто-relink не запускаем — пользователю сообщается, что запись существует и её нужно привязать заново.
  • Команда «Закрыть» — универсальный стоп-кран. Добавляется ручная команда, доступная из любого состояния (кроме deleted) во всех транспортах, переводящая запись в терминальное cancelled (с error_code-дискриминатором) и убирающая её из активного списка. Команда только меняет статус: файлы под paths.* не трогает (в т.ч. из done/orphaned библиотечные хардлинки сознательно остаются на месте) и раздачу в qBittorrent не снимает (в отличие от «Удалить»). Размещается в отдельной danger zone внизу страницы. Так пользователь закрывает лишнего близнеца, а заодно получает страховку для любой зависшей/спорной загрузки.

Явно вне scope: авто-схлопывание дублей в фоновой сверке (выбран ручной путь); изменение логики усыновления в воркере; введение нового статуса (переиспользуем cancelled, как Delete переиспользует deleted); удаление/создание каких-либо файлов или раздач командой «Закрыть».

Capabilities

New Capabilities

Modified Capabilities

  • ingest: критерий дедупликации приёма расширяется — блокирующими повторный приём становятся не только активные, но и target_missing/orphaned записи (attach вместо создания новой); повторный приём завершённой (done) остаётся созданием новой. Модифицируются требования «Дедупликация приёма по любому из хешей» и «Приём источника и заведение загрузки» (терминология «блокирующей» задачи). Требование «Приём из .torrent-файла» текст НЕ правит: оно уже явно делегирует критерий модифицированному требованию через inline-ссылку — оба дедуп-упоминания там наследуют расширенный критерий без риска дрейфа.
  • state-reconciliation: добавляется пользовательская команда «Закрыть» (dismiss) из любого состояния (кроме deleted) в терминальное cancelled, ничего не делающая с файлами и раздачей; фиксируется её отличие от «Удалить» и новые рёбра перехода <любое> → cancelled (в т.ч. из терминальных done/failed/reverted/target_missing/orphaned).

Impact

  • Код: internal/ingest/ingest.go (Ingest/attached — ветка attach для desync-записей, флаг «нужен relink»), internal/store/download.go (критерий поиска дедупа: reingest-blocking states = active {target_missing, orphaned}; новое ребро allowedTransitions target_missing → cancelled; error_code-дискриминатор dismiss), internal/worker/review.go (обработчик команды «Закрыть» рядом с Delete/Undo/Relink — только setState, без файлов и qBittorrent), веб-UI (danger zone внизу страницы с кнопкой «Закрыть»), internal/tgbot.
  • Данные: новых таблиц/столбцов нет; терминальный набор не меняется (dismiss → существующее cancelled). Обновляется граф переходов и, при необходимости, описание статусов в docs/specs/database.md.
  • Инварианты безопасности данных: «Закрыть» источник неприкосновенен — qBittorrent не вызывается, файлы под paths.downloads/movies/series не трогаются.