# Конвенции раскладки 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`.
- **Сходимость папки:** при подтверждённом матче база папки (имя+год) наследуется
  от живой папки-якоря того же `(provider, provider_id)` (существующей на диске), а
  не печатается заново из выхода LLM — так второй сезон ложится в ту же папку, что
  и первый. Несколько разных живых папок одного матча → review. Источник истины —
  `openspec/specs/file-layout/` («Сходимость базы папки…»).

## Сопоставление источник → цель

Источник берём по пути из 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)).
