mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
Initial import: vesti.nixg.ru — новостной апрув-проект (web, crawler, classifier, publisher, openspec)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-13
|
||||
skip_specs: true
|
||||
@@ -0,0 +1,60 @@
|
||||
## Design
|
||||
|
||||
### Роут в web/app.py
|
||||
|
||||
После `/bundle/...` (в конце файла) добавить:
|
||||
|
||||
```python
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
MEDIA_DIRS = [
|
||||
BASE_DIR / "media", # свежие: media/<file>
|
||||
BASE_DIR / "media" / "media" # старые: media/media/<file>
|
||||
]
|
||||
|
||||
@app.get("/media/{filename}")
|
||||
def media(request: Request, filename: str):
|
||||
"""Отдаёт медиа-файл поста (из media/ или media/media/). Авторизация."""
|
||||
_require_auth(request)
|
||||
name = os.path.basename(filename) # защита от path traversal
|
||||
if not name:
|
||||
return HTMLResponse("bad filename", status_code=400)
|
||||
for d in MEDIA_DIRS:
|
||||
f = (d / name).resolve()
|
||||
if f.exists() and f.is_file():
|
||||
# отдаём с корректным MIME по расширению
|
||||
return FileResponse(f)
|
||||
return HTMLResponse("not found", status_code=404)
|
||||
```
|
||||
|
||||
Примечание: `FileResponse` уже есть в fastapi.responses (импортировать).
|
||||
`MEDIA_DIRS` можно вынести в константы рядом с `STATIC_DIR`.
|
||||
|
||||
### Шаблоны
|
||||
|
||||
В `candidates.html` (и `published.html`), заменить блок бейджа:
|
||||
|
||||
```html
|
||||
{% if p.media_path %}
|
||||
<span class="badge bg-light text-dark ms-2">🖼 медиа</span>
|
||||
{# маленькое превью: изображение или видео #}
|
||||
{% set media_src = '/media/' ~ p.media_path.split('/')[-1] %}
|
||||
{% if p.media_path.lower().endswith(('.jpg','.jpeg','.png','.gif','.webp')) %}
|
||||
<img src="{{ media_src }}" class="img-fluid rounded mt-2" style="max-height:180px" alt="медиа">
|
||||
{% elif p.media_path.lower().endswith(('.mp4','.webm','.mov')) %}
|
||||
<video src="{{ media_src }}" controls class="mt-2" style="max-height:180px"></video>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
В `candidates.html` строка 49: заменить бейдж на блок с превью.
|
||||
В `published.html` — аналогично (там сейчас бейдж медиа? проверить).
|
||||
|
||||
### Верификация
|
||||
|
||||
- `openspec validate web-media-preview` — чисто.
|
||||
- Перезапуск веба: `sudo systemctl restart vesti-web`.
|
||||
- Открыть /candidates — у поста с media_path видно изображение/видео.
|
||||
- `/media/LinuxMastery_1079.jpg` — 200 (файл есть).
|
||||
- Старый пост с media/media/<file> — тоже 200.
|
||||
- Несуществующий файл — 404.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
Пользователь не видит, что за картинка приложена к новости в карточке кандидата:
|
||||
в шаблоне `candidates.html` для постов с `media_path` показывается только бейдж
|
||||
«🖼 медиа», а само изображение не отображается. В `web/app.py` нет роута, который
|
||||
отдаёт медиа-файл (есть только /static для bootstrap), поэтому `<img>` некуда указывать.
|
||||
Аналогично в `published.html` медиа не показывается.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Добавить в `web/app.py` роут `GET /media/{filename}` (с авторизацией, как у других
|
||||
роутов), который отдаёт файл из `/opt/vesti/media/` или `/opt/vesti/media/media/`
|
||||
(исторический баг путей: у старых постов media_path = `media/media/<file>`).
|
||||
Безопасно: только basename (защита от path traversal), отдаём FileResponse.
|
||||
- В `candidates.html` и `published.html` для постов с `media_path` выводить
|
||||
`<img src="/media/{{ basename(media_path) }}" class="img-fluid ...">`
|
||||
(направление на роут; если файла нет — не показывать/плейсхолдер).
|
||||
- Медиа в карточке: фото/видео. Для изображений — `<img>`, для видео — `<video controls>`.
|
||||
|
||||
## Why Not
|
||||
|
||||
- Отдавать медиа через /static нельзя: файлы вне static/ и большие; роут нужен именно
|
||||
для media/.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Путь `media_path` в БД: `media/<file>` или `media/media/<file>` — резолвить через
|
||||
basename (имя файла уникально в каталоге).
|
||||
@@ -0,0 +1,10 @@
|
||||
# web-media-preview
|
||||
|
||||
- [x] Создан OpenSpec change (proposal/design)
|
||||
- [x] web/app.py: роут `/media/{filename}` (FileResponse, защита path traversal) + MEDIA_DIRS
|
||||
- [x] candidates.html: превью медиа (img/video) вместо бейджа
|
||||
- [x] published.html: превью медиа
|
||||
- [x] `openspec validate web-media-preview` — чисто (skip_specs: true, валиден)
|
||||
- [x] Перезапуск веба; /media/LinuxMastery_1079.jpg → 200 image/jpeg; mp4 (старый) → 200 video/mp4; missing → 404
|
||||
- [x] Бэкап после правки (`sudo /opt/vesti/backup.sh`)
|
||||
- [x] Клик по картинке → полноразмер в новой вкладке (`<a target="_blank">` вокруг `<img>`, без JS)
|
||||
Reference in New Issue
Block a user