Tududi оказался неудобен для ведения беклога проекта — переходим на файлы в репозитории. Каждая задача — отдельный markdown в docs/backlog/ (48 файлов), плюс индекс README.md со списком по приоритетам и хуками. Тело файла хранит исходное описание (контекст, решения, ссылки на спеки/ADR/черновики). CLAUDE.md: источник истины по беклогу теперь docs/backlog/; Tududi понижен до инбокса сырых идей. Живые ссылки в спеках (recognition, architecture, review-ux, jellyfin-layout) «в беклоге (Tududi)» переписаны на прямые ссылки на файлы беклога. Перенесённые задачи удалены из Tududi; завершённые оставлены как история. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
7.0 KiB
Markdown
103 lines
7.0 KiB
Markdown
# Конвенции раскладки Jellyfin
|
||
|
||
> **Источник истины переехал в OpenSpec** — `openspec/specs/file-layout/` (имена,
|
||
> хардлинки, коллизия, copy-fallback). Владение путём (`superseded`) и безопасный
|
||
> undo (`nlink<=1`) — в `openspec/specs/state-reconciliation/`. Этот файл —
|
||
> справочный нарратив; при расхождении верна спека OpenSpec.
|
||
|
||
Целевые имена и структура, в которые jellybit раскладывает файлы
|
||
хардлинками. Источники:
|
||
[Movies](https://jellyfin.org/docs/general/server/media/movies),
|
||
[Shows](https://jellyfin.org/docs/general/server/media/shows).
|
||
|
||
## Фильмы
|
||
|
||
```
|
||
movies/
|
||
Дюна Часть вторая (2024) [tmdbid-693134]/
|
||
Дюна Часть вторая (2024).mkv
|
||
Дюна Часть вторая (2024).ru.srt
|
||
```
|
||
|
||
- Папка и файл — `Название (Год)`.
|
||
- provider-id в имени папки (`[tmdbid-...]`) добавляется при работе с
|
||
базой — снимает неоднозначность для русских названий, которые Jellyfin
|
||
иначе может опознать неверно.
|
||
- Внешние субтитры — `Имя.<lang>[.flag].srt` (флаги `forced`/`sdh`/
|
||
`default`/`hi`), напр. `…ru.forced.srt`; база имени совпадает с именем
|
||
видеофайла. Пары VobSub — `.idx` + `.sub`.
|
||
|
||
## Сериалы
|
||
|
||
```
|
||
series/
|
||
Название (2024) [tvdbid-123456]/
|
||
Season 01/
|
||
Название (2024) S01E01.mkv
|
||
Название (2024) S01E02.mkv
|
||
```
|
||
|
||
- provider-id — на папке сериала.
|
||
- Сезоны — `Season 01`, файлы — `... SxxEyy`.
|
||
|
||
## Сопоставление источник → цель
|
||
|
||
Источник берём по пути из qBittorrent (`save_path` + относительное имя
|
||
файла из `/torrents/files`, которое уже содержит корневую папку
|
||
многофайловой раздачи; это уже хост-путь, `path_map` — фолбэк). Для каждого
|
||
распознанного **файла** (не каталога) создаётся **хардлинк** в
|
||
`paths.movies`/`paths.series`; целевые каталоги — `mkdir` (0755,
|
||
`1000:1000`). Исходный файл остаётся на месте (раздача продолжается),
|
||
inode общий — диск не дублируется.
|
||
|
||
Целевое имя строится из распознанных полей и **санитизируется** (без
|
||
разделителей пути, `..`, управляющих символов); финальный путь обязан
|
||
быть строго под библиотекой. Существующую цель **не перезаписываем** (тот
|
||
же inode → готово; другой файл → коллизия → review). Инварианты и undo —
|
||
в [architecture.md](architecture.md) → «Раскладка файлов».
|
||
|
||
## Владение целевым путём
|
||
|
||
Целевой путь принадлежит **одной** загрузке. Когда новая раскладка
|
||
успешно ложится на путь, который раньше занимала другая загрузка (путь к
|
||
этому моменту **свободен** — иначе была бы коллизия → review, чужой файл
|
||
не перезаписываем), владение переходит к новой загрузке: прежние записи
|
||
`file_link` на этот путь помечаются статусом `superseded` и перестают
|
||
считаться целью прежней загрузки. Это нужно сверке с реальностью: иначе
|
||
повторная закачка того же фильма (например, в другом качестве) по тому же
|
||
пути ложно «воскрешала» бы удалённую задачу — см.
|
||
[workflow.md](workflow.md) → «Сверка с реальностью». `superseded`-ссылки
|
||
не считаются целью при сверке и не снимаются в `Undo`.
|
||
|
||
Желательно: целевой и исходный каталоги — на одной ФС/одном mount'е
|
||
(внутри контейнера это обеспечивает единая песочница `/srv/media`), тогда
|
||
работает дешёвый хардлинк. Если хардлинк невозможен (разные ФС или ФС без
|
||
поддержки жёстких ссылок), `layout` не падает, а копирует файл с
|
||
предупреждением в лог — см. architecture.md → «Раскладка файлов».
|
||
|
||
## Безопасный undo (не снимать последнюю копию)
|
||
|
||
`Undo` снимает **лишний** хардлинк, а не единственный файл. Перед удалением
|
||
батча `layout` проверяет каждую цель: если исходный файл уже не существует
|
||
**или** у цели не осталось других жёстких ссылок (`nlink <= 1`), это —
|
||
последняя копия данных, и весь `Undo` отклоняется целиком (ошибка
|
||
`ErrLastCopy`), не сняв ни одной ссылки (частичный откат тоже стёр бы часть
|
||
данных). Так нарушенный инвариант «источник неприкосновенен» (источник
|
||
удалён вручную) не приводит к потере данных. Отсутствующую цель `Undo`
|
||
пропускает как уже снятую (идемпотентность). Связь с состояниями
|
||
рассинхрона — [workflow.md](workflow.md) → «Сверка с реальностью».
|
||
|
||
## Крайние случаи
|
||
|
||
- **Многофайловый фильм** (части) — стэкинг по точному токену Jellyfin
|
||
(`… - part1`/`cd1`); точный формат уточнить при реализации.
|
||
- **Редакции** — `Имя (Год) [edition-Director's Cut]` либо отдельные
|
||
версии в папке фильма.
|
||
- **Двойная серия** в одном файле — `… SxxEyy-Eyy`.
|
||
- **Спецвыпуски** — `Season 00`.
|
||
- **Сезон-пак** — серии в один `Season xx`; смешанный пак — по per-file
|
||
сезонам.
|
||
- **Несколько аудиодорожек** — обычно внутри mkv, не наша забота.
|
||
- **Аниме с абсолютной нумерацией** — пересчёт в S·E, отдельная проработка
|
||
([задача в беклоге](../backlog/anime-absolyutnaya-numeraciya.md)).
|