Files
jellybit/docs/specs/database.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

142 lines
7.7 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.
# Схема базы данных
Актуальная схема SQLite-хранилища: таблицы, поля и связи. Это **живой**
документ — его поддерживаем в соответствии с миграциями.
> **Поддержка вместе с миграциями.** Источник истины по схеме —
> `internal/store/migrations/*.sql` (goose). При **каждой** новой миграции,
> меняющей структуру (таблица/столбец/индекс/связь), обновляем эту диаграмму
> в том же change. Расхождение схемы с миграциями считаем багом
> документации.
>
> Состояние на: миграции `0001_init`, `0002_recognition_plan`,
> `0003_source_miss_count`, `0004_candidate_url`, `0005_display_name`,
> `0006_ulid_identity` (Go-миграция: ULID-идентификаторы, `download_infohash`).
Назначение таблиц и почему так — [architecture.md](architecture.md) →
«Хранилище». Значения `state` и переходы — [workflow.md](workflow.md).
Первичные ключи — ULID (TEXT, lowercase), генерятся приложением
(`internal/ident`) — см. [конвенцию](../conventions/database.md).
## ER-диаграмма
```mermaid
erDiagram
download ||--o{ download_infohash : "инфохэши (v1/v2)"
download ||--o{ recognition : "распознавания"
download ||--o{ hint : "подсказки"
download ||--o{ override : "ручные правки"
download ||--o{ file_link : "хардлинки"
recognition ||--o{ metadata_candidate : "кандидаты базы"
download {
TEXT id PK "ULID (lowercase), генерится приложением"
TEXT source_type "NOT NULL; magnet|torrent|url"
TEXT source_ref "NOT NULL; magnet/url/путь"
TEXT display_name "NOT NULL DEFAULT ''; имя раздачи (rename qBittorrent), заголовок в UI (миграция 0005)"
TEXT context "NOT NULL DEFAULT ''"
TEXT state "NOT NULL; см. workflow.md; активность выводится только из state"
TEXT error_code "nullable"
TEXT error_msg "nullable"
INTEGER source_miss_count "NOT NULL DEFAULT 0; дебаунс пропажи источника (миграция 0003)"
TEXT source_added_at "nullable; время добавления в qBittorrent (added_on), базис сортировки (миграция 0005)"
TEXT created_at "NOT NULL DEFAULT datetime('now')"
TEXT updated_at "NOT NULL DEFAULT datetime('now')"
}
download_infohash {
TEXT download_id PK_FK "NOT NULL; ON DELETE CASCADE; PK(infohash, download_id)"
TEXT infohash PK "NOT NULL; lowercase hex (40 — v1, 64 — v2)"
TEXT kind "NOT NULL; v1|v2"
TEXT created_at "NOT NULL DEFAULT datetime('now')"
}
recognition {
TEXT id PK "ULID"
TEXT download_id FK "NOT NULL; ON DELETE CASCADE"
INTEGER attempt_no "NOT NULL DEFAULT 1"
INTEGER is_current "NOT NULL DEFAULT 1; 0/1"
TEXT media_type "nullable; movie|series"
TEXT title "nullable"
TEXT original_title "nullable"
INTEGER year "nullable"
TEXT provider "nullable; tmdb|tvdb|tvmaze|none"
TEXT provider_id "nullable"
REAL confidence "nullable"
TEXT reasons "NOT NULL DEFAULT '[]'; JSON: причины не-авто"
TEXT raw_llm "nullable; сырой ответ LLM"
TEXT plan "nullable; JSON recognize.Plan (миграция 0002)"
TEXT created_at "NOT NULL DEFAULT datetime('now')"
}
hint {
TEXT id PK "ULID"
TEXT download_id FK "NOT NULL; ON DELETE CASCADE"
TEXT text "NOT NULL"
TEXT created_at "NOT NULL DEFAULT datetime('now')"
}
override {
TEXT id PK "ULID"
TEXT download_id FK "NOT NULL; ON DELETE CASCADE"
TEXT field "NOT NULL; UNIQUE(download_id, field)"
TEXT value "NOT NULL"
TEXT created_at "NOT NULL DEFAULT datetime('now')"
}
metadata_candidate {
TEXT id PK "ULID"
TEXT recognition_id FK "NOT NULL; ON DELETE CASCADE"
TEXT provider "NOT NULL"
TEXT provider_id "NOT NULL"
TEXT title "nullable"
INTEGER year "nullable"
TEXT url "nullable; ссылка на страницу на сайте провайдера"
INTEGER chosen "NOT NULL DEFAULT 0; 0/1"
TEXT created_at "NOT NULL DEFAULT datetime('now')"
}
file_link {
TEXT id PK "ULID"
TEXT download_id FK "NOT NULL; ON DELETE CASCADE"
TEXT apply_batch_id "NOT NULL; батч для точечного undo"
TEXT src_path "NOT NULL; исходный файл раздачи"
TEXT dst_path "NOT NULL; целевой хардлинк"
TEXT kind "NOT NULL; video|subtitle|..."
TEXT status "NOT NULL; linked|..."
INTEGER size "NOT NULL DEFAULT 0; размер файла (байт), фолбэк размера раздачи"
TEXT created_at "NOT NULL DEFAULT datetime('now')"
}
```
## Связи и кардинальность
- `download` 1 — N `download_infohash` / `recognition` / `hint` / `override`
/ `file_link`; `recognition` 1 — N `metadata_candidate`. Все дочерние — с
`ON DELETE CASCADE`: удаление загрузки уносит её хеши, распознавания,
подсказки, правки и ссылки.
- `download_infohash` — множество хешей одной загрузки (v1/v2 гибридного
торрента); один и тот же infohash может принадлежать нескольким загрузкам
во времени (повторный приём после терминального состояния). Инвариант «не
более одной активной загрузки на infohash» держат guarded-методы store
(`CreateDownloadIfNoActive`/`ActivateIfNoOtherActive`) в одной
write-транзакции — на уровне схемы он не выражается (условие на `state`).
- `download``file_link` — один источник (раздача) ко многим разложенным
файлам; внутри строки `file_link` связь `src_path → dst_path` — 1:1. Не
каждый файл раздачи попадает в `file_link` (только распознанные медиа и
субтитры); ссылки могут накапливаться несколькими `apply_batch_id`.
## Индексы и ограничения
- `download`: индекс по `state`.
- `download_infohash`: PK `(infohash, download_id)` (он же индекс поиска по
хешу); индекс по `download_id`.
- `recognition`: индекс по `download_id`.
- `override`: `UNIQUE(download_id, field)`.
- `metadata_candidate`: индекс по `recognition_id`.
- `file_link`: индексы по `download_id` и по `apply_batch_id`.
> Enum-поля (`source_type`, `state`, `provider`, `kind`, `status`, флаги
> `0/1`) на уровне SQLite — обычный `TEXT`/`INTEGER` без `CHECK`; допустимые
> значения держит код (`internal/store`).