Files
jellybit/openspec/specs/file-layout/spec.md
T
avandClaude Opus 4.8 bb245a90a3 Веб-UI: обзор жизненного цикла в карточке загрузки
Карточка списка на главной теперь даёт краткий обзор «от загрузки до
решения об удалении»: метка «ID:» перед идентификатором, дата добавления
(абсолютная + относительная, всегда), размер раздачи и рейтинг отдачи.
Спойлер контекста убран — контекст смотрят на /download/{id}.

Данные:
- рейтинг и общий размер — из живого снимка воркера (qbt total_size →
  worker.Live.TotalSize); размер доступен для любой раздачи в снимке;
- размер-фолбэк, когда торрента нет в qBittorrent (orphaned) — сумма
  размеров разложенных файлов: новая колонка file_link.size, layouter
  пишет размер при линковке, ридер LayoutSizeByDownload суммирует по
  странице одним запросом (дедуп по dst_path);
- дата — source_added_at → фолбэк created_at, показ в TZ сервера.

handleIndex читает снимок для всех карточек (map-lookup), рейтинг/размер
статичны на рендере (без поллинга). Миграция 0007, ER-схема обновлена.
Change download-card-lifecycle-overview влит в спеки и заархивирован.

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

114 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# file-layout Specification
## Purpose
Раскладка распознанных файлов хардлинками под библиотеку Jellyfin: целевые
имена фильмов и сериалов (папка/файл, provider-id, сезоны), сопоставление
источник→цель, санитизация пути и запрет выхода за библиотеку, never-overwrite
(коллизия → review) и copy-fallback при невозможности хардлинка. Владение
целевым путём (`superseded`) и безопасный `Undo` (`nlink<=1`) —
в `state-reconciliation`.
## Requirements
### Requirement: Целевые имена фильмов
Фильм система SHALL раскладывать в папку и файл вида `Название (Год)`, помещённые
под `paths.movies`. При подтверждённом матче в базе имя папки SHALL нести
provider-id (`[tmdbid-…]`/`[tvdbid-…]`) — он снимает неоднозначность русских
названий для Jellyfin. Внешние субтитры SHALL именоваться `Имя.<lang>[.flag].srt`
(флаги `forced`/`sdh`/`default`/`hi`), с базой имени, совпадающей с именем
видеофайла; пары VobSub — `.idx` + `.sub`.
#### Scenario: Фильм с provider-id
- **GIVEN** распознанный фильм «Дюна Часть вторая» (2024) с матчем TMDB `693134`
- **WHEN** строится целевой путь
- **THEN** папка = `movies/Дюна Часть вторая (2024) [tmdbid-693134]/`
- **AND** видеофайл = `Дюна Часть вторая (2024).mkv`
### Requirement: Целевые имена сериалов
Сериал система SHALL раскладывать под `paths.series` в папку `Название (Год)` с
provider-id на папке сериала, сезонными подпапками `Season xx` и файлами вида
`Название (Год) SxxEyy`.
#### Scenario: Серия сезона
- **GIVEN** распознанный сериал «Фарго» (2024) с матчем TVDB `123456`, серия S01E02
- **WHEN** строится целевой путь
- **THEN** путь = `series/Фарго (2024) [tvdbid-123456]/Season 01/Фарго (2024) S01E02.mkv`
### Requirement: Сопоставление источник → цель хардлинками
Для каждого распознанного **файла** (не каталога) система SHALL создавать
**хардлинк** в `paths.movies`/`paths.series`; исходный путь берётся из
qBittorrent (`save_path` + относительное имя файла из `/torrents/files`, уже
включающее корневую папку многофайловой раздачи). Целевые каталоги SHALL
создаваться `mkdir` (0755, `1000:1000`). Исходный файл система НЕ SHALL трогать —
раздача продолжается, inode общий, диск не дублируется.
#### Scenario: Хардлинк не дублирует данные
- **GIVEN** видеофайл раздачи под `paths.downloads`
- **WHEN** файл раскладывается
- **THEN** в библиотеке создаётся хардлинк на тот же inode
- **AND** исходный файл остаётся на месте
### Requirement: Санитизация целевого пути и запрет выхода за библиотеку
Целевое имя система SHALL санитизировать (без разделителей пути, `..`,
управляющих символов), а финальный путь SHALL проверять на строгое нахождение под
`paths.movies`/`paths.series`. Путь, выходящий за пределы библиотеки, система НЕ
SHALL создавать. Безопасность SHALL держаться на валидации пути, а не на доверии к
выходу LLM.
#### Scenario: Traversal отклоняется
- **GIVEN** распознанное имя, содержащее `../`
- **WHEN** строится и проверяется целевой путь
- **THEN** путь отклоняется как выходящий за пределы библиотеки, хардлинк не создаётся
### Requirement: Существующую цель не перезаписываем
Существующий целевой файл система НЕ SHALL перезаписывать. Если по целевому пути
уже лежит тот же inode — операция идемпотентна (готово); если другой файл —
это коллизия, и задача SHALL уходить в `review`.
#### Scenario: Коллизия уходит в review
- **GIVEN** по целевому пути уже лежит другой файл
- **WHEN** выполняется раскладка
- **THEN** файл не перезаписывается, задача переходит в `review` с причиной коллизии
### Requirement: Copy-fallback при невозможности хардлинка
Система SHALL при невозможности хардлинка (разные ФС или ФС без поддержки жёстких
ссылок) НЕ падать, а копировать файл с предупреждением в лог, помечая ссылку
статусом `copied`.
#### Scenario: Разные ФС — копирование
- **GIVEN** целевой и исходный каталоги на разных ФС
- **WHEN** выполняется раскладка файла
- **THEN** файл копируется, ссылка получает статус `copied`, в лог пишется предупреждение
### Requirement: Запись размера разложенного файла
При линковке файла раскладка SHALL сохранять его размер в байтах вместе с
записью о созданной ссылке (`file_link`). Сохранённый размер SHALL позволять
вычислить суммарный размер разложенных файлов загрузки — он служит фолбэком
размера раздачи, когда торрента нет в qBittorrent (например, состояние
`orphaned`, где файлы библиотеки — последняя копия данных). Запись размера MUST
NOT влиять на инвариант неприкосновенности источника (по-прежнему только
`mkdir`/`link`/копия в цель).
#### Scenario: Размер сохраняется при линковке
- **WHEN** раскладка создаёт хардлинк (или копию при copy-fallback) файла
- **THEN** размер этого файла в байтах сохраняется в записи ссылки `file_link`
#### Scenario: Суммарный размер доступен без торрента
- **WHEN** у загрузки есть разложенные файлы, а её торрента нет в qBittorrent
- **THEN** суммарный размер разложенных файлов доступен как размер раздачи для
показа в карточке