Files
jellybit/web/templates/index.html
T
avandClaude Opus 4.8 14d615a7c2 Приём: добавление загрузки по .torrent-файлу
Принимаем .torrent как загруженные байты — через файл-пикер в веб-форме и
Telegram-документ, наряду с magnet. Файл несёт полные метаданные: работает
там, где magnet не резолвится (закрытые трекеры, без DHT), и даёт максимум
контекста для распознавания без сети.

- internal/torrent: парсер поверх anacrolix/torrent/metainfo — инфохэш(и)
  (v1 SHA1 исходных байтов info; v2 BEP52 при наличии) + Context() из имени,
  дерева файлов, размера, трекеров. Извлечение файлов панико-безопасно
  (недоверенный вход).
- Персистентность байтов: таблица-спутник download_torrent (миграция 0009);
  пишется в транзакции создания загрузки, только на ветке создания (не при
  дедупе). Байты живут весь срок строки — нужны для повторного добавления
  при retry.
- ingest: Request.TorrentData/TorrentName, диспетч парсера; source_ref —
  человекочитаемый референс (имя раздачи/файла), не адрес добавления.
- worker: общий sourceAddParts ветвит по source_type в ОБОИХ add-путях —
  processCatched и Retry (torrent добавляется файлом, не magnet-хешем).
- Транспорты: multipart-форма с файл-пикером (деградация без JS) и приём
  Telegram-документа (скачивание с редактированием токена из ошибок — секрет
  не в логи; обработка до ветки pending/текста).

Разработка по OpenSpec (SDD): change torrent-file-ingest, два чекпоинта ревью
(дизайн до кода, код до архива) сабагентами; дельты влиты в спеки, change
архивирован. Ручная проверка на живом qBittorrent (7.3) — за деплоем.

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

80 lines
3.6 KiB
HTML

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jellybit — загрузки</title>
<link rel="stylesheet" href="{{asset "css/jellybit.css"}}">
<script src="{{asset "vendor/htmx.min.js"}}" defer></script>
<script src="{{asset "js/app.js"}}" defer></script>
</head>
<body>
{{template "header" "downloads"}}
<main class="main">
<h1 class="page-title">Загрузки</h1>
<p class="page-sub">Кинь magnet, ссылку, .torrent или сообщение бота — остальное jellybit сделает сам.</p>
{{if .Error}}<div class="result-banner" style="background:var(--st-err-bg);border-color:color-mix(in srgb,var(--st-err) 35%,transparent)"><h2 style="color:var(--st-err)">{{.Error}}</h2></div>{{end}}
<!-- Единое окно добавления -->
<form class="add-box" method="post" action="/ui/downloads" enctype="multipart/form-data">
<div class="add-row">
<input class="input" type="text" name="source" autocomplete="off"
placeholder="magnet:?xt=urn:btih:… · ссылка · или вставь текст бота">
<label class="btn btn-file" title="Выбрать .torrent-файл">
<span data-file-label>.torrent</span>
<input type="file" name="torrent" accept=".torrent,application/x-bittorrent" hidden
onchange="var l=this.closest('.btn-file'),s=l.querySelector('[data-file-label]');s.textContent=this.files.length?this.files[0].name:'.torrent';l.classList.toggle('has-file',!!this.files.length)">
</label>
<button class="btn btn-primary btn-lg" type="submit">Добавить</button>
</div>
<p class="add-hint">Вставь magnet/ссылку или сообщение трекер-бота, либо выбери <code>.torrent</code>-файл — контекст разберётся автоматически.</p>
</form>
{{if or .Downloads .Searching}}
<!-- Панель фильтров (серверные: фильтр/поиск/пагинация в query) -->
<div class="toolbar" id="list-toolbar">
<form class="search" method="get" action="/">
<span class="ico"></span>
<input class="input" type="search" name="q" value="{{.Query}}"
placeholder="Поиск по названию, id или infohash…">
{{if ne .Filter "all"}}<input type="hidden" name="f" value="{{.Filter}}">{{end}}
{{if .ShowAll}}<input type="hidden" name="all" value="1">{{end}}
</form>
<div class="filter-chips" id="chips">
{{range .Chips}}<a class="chip{{if .Active}} active{{end}}" href="{{.URL}}">{{.Label}}</a>{{end}}
</div>
</div>
<div class="toggle-line">
<a href="{{.ShowURL}}">{{if .ShowAll}}☑{{else}}☐{{end}} Показать всё, включая удалённые</a>
</div>
{{end}}
{{if .Downloads}}
<!-- Список карточек -->
<div id="list">
{{range .Downloads}}{{template "card" .}}{{end}}
</div>
{{if gt .Pages 1}}
<nav class="pager">
{{if .PrevURL}}<a class="btn btn-sm" href="{{.PrevURL}}"></a>{{end}}
{{range .PageLinks}}<a class="btn btn-sm{{if .Active}} btn-primary{{end}}" href="{{.URL}}">{{.Num}}</a>{{end}}
{{if .NextURL}}<a class="btn btn-sm" href="{{.NextURL}}"></a>{{end}}
</nav>
{{end}}
{{else}}
<div class="empty-state">
<div class="big">🍿</div>
{{if .Searching}}Ничего не найдено. Поменяй фильтр или запрос.{{else}}Пока пусто. Добавь первую загрузку выше.{{end}}
</div>
{{end}}
</main>
</body>
</html>