rpi-media: стартовая страница Homepage, Radarr/Sonarr → Jellyfin connect
- homepage/: сервисы с описанием и живыми виджетами; ключи — в homepage.env на малине - Jellyfin-виджет через customapi (встроенный не совместим с Jellyfin 12) - provision.py: шаги homepage и connect Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f70d5c848f
commit
94bc983401
@@ -6,7 +6,7 @@
|
||||
- [`servers/de1.md`](servers/de1.md) — 213.136.74.247. **Выведен** (все сервисы остановлены, метрики удалены): был прокси/VPN-нодой (telemt, AmneziaWG, OpenVPN) плюс gitea, openclaw, боты — всё уехало на fr1 или в бэкапы. ⚠️ Сам файл описания под вывод не переписан.
|
||||
- [`servers/fr1.md`](servers/fr1.md) — `fr1.ruzzy.dev` (161.97.93.252). Основная нода (Ubuntu 24.04): мониторинг, gitea, AmneziaWG exit для ru1, боты (claude-channels, openclaw), статика `www`/`share`. Держит апекс `ruzzy.dev`.
|
||||
- [`servers/ru1.md`](servers/ru1.md) — `85.198.109.86` (VDSina, РФ). VPN entry-нода (OpenVPN ×2, AmneziaWG **до fr1** после переключения, WireGuard клиентов), умный роутинг RU/зарубеж; WEB-прокси Telegram (tproxy-server + MTProxy) на `ru.ruzzy.dev:443`, telemt удалён 2026-10-09. ⚠️ Файл описания сверялся до переключения.
|
||||
- [`servers/rpi.md`](servers/rpi.md) — `rpi.local` (192.168.10.25, дом, за NAT). Raspberry Pi 4: бэкап личного OneDrive на HDD 4 ТБ (rclone, вечный архив версий, еженедельная сверка хэшей); SMART и статус бэкапов пушит в pushgateway fr1 по SSH. Код — `stacks/rpi-backup/`. Агент Astra (Codex) с управлением из Telegram — репо `ruzzy/astra-bot`. Медиацентр в Docker (Jellyfin, Seerr, qBittorrent, Radarr/Sonarr/Prowlarr/Bazarr) — `stacks/rpi-media/`.
|
||||
- [`servers/rpi.md`](servers/rpi.md) — `rpi.local` (192.168.10.25, дом, за NAT). Raspberry Pi 4: бэкап личного OneDrive на HDD 4 ТБ (rclone, вечный архив версий, еженедельная сверка хэшей); SMART и статус бэкапов пушит в pushgateway fr1 по SSH. Код — `stacks/rpi-backup/`. Агент Astra (Codex) с управлением из Telegram — репо `ruzzy/astra-bot`. Медиацентр в Docker (Jellyfin, Seerr, qBittorrent, Radarr/Sonarr/Prowlarr/Bazarr, стартовая страница Homepage :3000) — `stacks/rpi-media/`.
|
||||
|
||||
## Поддержание описаний в актуальном состоянии
|
||||
Тронул сервер — обнови `servers/<хост>.md` в том же прогоне. Контракт и
|
||||
|
||||
+5
-1
@@ -93,6 +93,7 @@ Docker CE 29.9 + Compose v5.6 из репо download.docker.com (в Debian ст
|
||||
| Prowlarr | 9696 | индексаторы, раздаёт их в Radarr/Sonarr | 2.6.5 |
|
||||
| FlareSolverr | 8191 | обход Cloudflare для индексаторов с тегом `flaresolverr` | v3.5.2 |
|
||||
| Bazarr | 6767 | субтитры, профиль `ru+en` | v1.6.2 |
|
||||
| Homepage | 3000 | **стартовая страница**: все сервисы с описанием по-русски, живой статус, CPU/температура/диск | v2.5.0 |
|
||||
|
||||
- **Данные — `/mnt/backup/media`** (4-ТБ диск, решение пользователя): `torrents/{movies,tv,other}`, `library/{movies,tv}`, `.cache/jellyfin` (кэш и транскоды Jellyfin — не на SD). qBittorrent и *arr монтируют **один и тот же** корень как `/data` — иначе импорт из `torrents/` в `library/` превращается из хардлинка в копию и раздача занимает место дважды. `copyUsingHardlinks` включён. В бэкапы (`router-backup`, `onedrive-*`) `media/` не входит.
|
||||
- ⚠️ **Медиа делят диск с бэкапами.** `onedrive-backup` падает с кодом 2 при заполнении >90 % (~370 ГБ свободно) → `BackupFailed`. В Radarr/Sonarr стоит `minimumFreeSpaceWhenImporting = 500 ГБ`: ниже этого импорт останавливается. **qBittorrent своего порога не имеет** — ручная заливка торрентов мимо *arr может забить диск.
|
||||
@@ -102,7 +103,10 @@ Docker CE 29.9 + Compose v5.6 из репо download.docker.com (в Debian ст
|
||||
- ⚠️ RuTracker закрывает `login.php`/`tracker.php` челленджем Cloudflare (по IPv4; IPv6 у малины нет маршрута). FlareSolverr на Pi решает его **80–180 с** — у прокси в Prowlarr `requestTimeout=180` (было 60, с ним добавление падало по таймауту). После решения Prowlarr держит cookie и поиск идёт ~4 с; когда cookie протухнет, первый поиск снова будет минутами, и *arr могут на время пометить индексатор недоступным.
|
||||
- Bazarr: провайдеры без аккаунтов (`embeddedsubtitles`, `podnapisi`, `yifysubtitles`); для русских субтитров лучший — OpenSubtitles.com, нужен аккаунт.
|
||||
- **Авторизация:** везде пользователь `rus`, один пароль — `/opt/media/.admin-password` (rus 600). *arr — формы, пароль **всегда** (режим «без пароля из LAN» в новых *arr требует `AllowedHosts`). Между собой сервисы ходят по API-ключам из `config/<app>/config.xml`.
|
||||
- Первичная связка — `stacks/rpi-media/provision.py` (идемпотентный, пароль и ключи читает на месте): `python3 /opt/media/provision.py [qbit jellyfin arr prowlarr bazarr seerr]`.
|
||||
- **Точка входа для пользователя — http://192.168.10.25:3000** (Homepage, конфиги `stacks/rpi-media/homepage/` → `/opt/media/homepage/`). Ключи виджетов — `/opt/media/homepage.env` (rus 600, `HOMEPAGE_VAR_*`, генерит `provision.py homepage`). ⚠️ Там же пароль qBittorrent: после смены общего пароля перегенерировать env и `docker compose up -d homepage`, иначе виджет qBittorrent отвалится. `HOMEPAGE_ALLOWED_HOSTS` обязателен — без него 400 на любой Host, кроме localhost.
|
||||
- ⚠️ Встроенный виджет `jellyfin` Homepage 2.5 с Jellyfin 12 не работает: ходит на `/emby/…?api_key=` — пути нет (404), ключ в query отключён (`EnableLegacyAuthorization=false`, включать не стали). Вместо него `customapi` на `/Items/Counts` с заголовком `Authorization: MediaBrowser Token=…`.
|
||||
- Radarr/Sonarr → Jellyfin (Connect «Jellyfin», тип MediaBrowser, ключ Jellyfin `homepage`; `provision.py connect` на чистой установке заводит ключ `arr-connect`): библиотека обновляется сразу после импорта. Без этого новый фильм висел невидимым до скана по расписанию — realtime-монитор хардлинк не заметил (проверено 2026-10-11 на первом фильме).
|
||||
- Первичная связка — `stacks/rpi-media/provision.py` (идемпотентный, пароль и ключи читает на месте): `python3 /opt/media/provision.py [qbit jellyfin arr prowlarr bazarr seerr connect homepage]`.
|
||||
- ⚠️ Каталог `config/<сервис>`, которого нет до `up`, docker создаёт **от root** — Jellyfin/Seerr (uid 1000) падают с `Permission denied`. Новые каталоги создавать заранее или `chown -R rus:rus /opt/media/config`.
|
||||
- ⚠️ `fallenbagel/jellyseerr` заброшен (последний образ 2025-08) и не логинится в Jellyfin 12 — заменён на `ghcr.io/seerr-team/seerr`.
|
||||
- Торренты идут **напрямую** с домашнего IP (решение пользователя 2026-10-11; вариант через AWG fr1 отложен). Порт 6881 на роутере **не проброшен** — входящих соединений нет, раздача и скорость хуже.
|
||||
|
||||
@@ -110,3 +110,21 @@ services:
|
||||
- ./config/seerr:/app/config
|
||||
ports:
|
||||
- "5055:5055"
|
||||
|
||||
homepage:
|
||||
# стартовая страница со всеми сервисами, описаниями и живым статусом
|
||||
image: ghcr.io/gethomepage/homepage:latest
|
||||
container_name: homepage
|
||||
restart: unless-stopped
|
||||
env_file: ./homepage.env # HOMEPAGE_VAR_* — API-ключи и пароль qBittorrent, rus 600, только на малине
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
PUID: "1000"
|
||||
PGID: "1000"
|
||||
# без этого отдаёт 400 на любой Host, кроме localhost
|
||||
HOMEPAGE_ALLOWED_HOSTS: 192.168.10.25:3000,rpi.local:3000,localhost:3000
|
||||
volumes:
|
||||
- ./homepage:/app/config
|
||||
- /mnt/backup:/mnt/backup:ro # только чтобы виджет resources видел заполнение диска
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,99 @@
|
||||
# Ключи — из /opt/media/homepage.env (HOMEPAGE_VAR_*), в репо их нет.
|
||||
# Ссылки (href) — для браузера, адрес малины; виджеты ходят изнутри docker-сети по именам
|
||||
# контейнеров, кроме Jellyfin (он в host-сети).
|
||||
|
||||
- Как это работает:
|
||||
- 1. Найти:
|
||||
icon: mdi-magnify
|
||||
description: "Открой Seerr, найди фильм или сериал (можно по-русски) и нажми Request"
|
||||
- 2. Скачать:
|
||||
icon: mdi-download
|
||||
description: "Radarr/Sonarr сами ищут раздачу на трекерах, qBittorrent качает. Минуты–час"
|
||||
- 3. Смотреть:
|
||||
icon: mdi-television-play
|
||||
description: "Готовое появляется в Jellyfin; субтитры ru/en Bazarr докачает сам"
|
||||
- Если застряло:
|
||||
icon: mdi-help-circle-outline
|
||||
description: "Radarr/Sonarr → Activity → Queue; красная карточка = раздачи нет → Interactive Search"
|
||||
|
||||
- Смотреть:
|
||||
- Jellyfin:
|
||||
icon: jellyfin.svg
|
||||
href: http://192.168.10.25:8096
|
||||
description: "Медиатека. На LG — приложение Jellyfin, на iPhone/iPad — Infuse или Swiftfin, сервер 192.168.10.25:8096"
|
||||
# ⚠️ встроенный виджет jellyfin (Homepage 2.5) ходит на /emby/…?api_key= — в Jellyfin 12 этого
|
||||
# пути нет (404), а ключ в query отключён (EnableLegacyAuthorization=false). Поэтому customapi.
|
||||
widget:
|
||||
type: customapi
|
||||
url: http://192.168.10.25:8096/Items/Counts
|
||||
headers:
|
||||
Authorization: 'MediaBrowser Token="{{HOMEPAGE_VAR_JELLYFIN_KEY}}"'
|
||||
mappings:
|
||||
- field: MovieCount
|
||||
label: Фильмы
|
||||
format: number
|
||||
- field: SeriesCount
|
||||
label: Сериалы
|
||||
format: number
|
||||
- field: EpisodeCount
|
||||
label: Серии
|
||||
format: number
|
||||
- Seerr:
|
||||
icon: jellyseerr.svg
|
||||
href: http://192.168.10.25:5055
|
||||
description: "Каталог «хочу посмотреть»: Request → скачается само. Статусы: Processing → Available"
|
||||
widget:
|
||||
type: jellyseerr
|
||||
url: http://seerr:5055
|
||||
key: "{{HOMEPAGE_VAR_SEERR_KEY}}"
|
||||
|
||||
- Добавить фильм или сериал:
|
||||
- Radarr — фильмы:
|
||||
icon: radarr.svg
|
||||
href: http://192.168.10.25:7878
|
||||
description: "Очередь: Activity → Queue. Выбрать раздачу вручную: карточка → Interactive Search"
|
||||
widget:
|
||||
type: radarr
|
||||
url: http://radarr:7878
|
||||
key: "{{HOMEPAGE_VAR_RADARR_KEY}}"
|
||||
- Sonarr — сериалы:
|
||||
icon: sonarr.svg
|
||||
href: http://192.168.10.25:8989
|
||||
description: "Новые серии отслеживаемых сериалов качает сам. Очередь: Activity → Queue"
|
||||
widget:
|
||||
type: sonarr
|
||||
url: http://sonarr:8989
|
||||
key: "{{HOMEPAGE_VAR_SONARR_KEY}}"
|
||||
- Bazarr — субтитры:
|
||||
icon: bazarr.svg
|
||||
href: http://192.168.10.25:6767
|
||||
description: "Субтитры ru/en для всего в библиотеке; чего не хватает — Wanted"
|
||||
widget:
|
||||
type: bazarr
|
||||
url: http://bazarr:6767
|
||||
key: "{{HOMEPAGE_VAR_BAZARR_KEY}}"
|
||||
|
||||
- Загрузки и служебное:
|
||||
- qBittorrent:
|
||||
icon: qbittorrent.svg
|
||||
href: http://192.168.10.25:8080
|
||||
description: "Сами загрузки: скорость, сиды. Торренты вручную сюда — мимо Jellyfin и контроля места"
|
||||
widget:
|
||||
type: qbittorrent
|
||||
url: http://qbittorrent:8080
|
||||
username: "{{HOMEPAGE_VAR_QBIT_USER}}"
|
||||
password: "{{HOMEPAGE_VAR_QBIT_PASS}}"
|
||||
enableLeechProgress: true
|
||||
- Prowlarr — трекеры:
|
||||
icon: prowlarr.svg
|
||||
href: http://192.168.10.25:9696
|
||||
description: "RuTracker, NoNaMe Club, TPB, YTS… Ручной поиск по всем трекерам сразу"
|
||||
widget:
|
||||
type: prowlarr
|
||||
url: http://prowlarr:9696
|
||||
key: "{{HOMEPAGE_VAR_PROWLARR_KEY}}"
|
||||
- FlareSolverr:
|
||||
icon: flaresolverr.svg
|
||||
href: http://192.168.10.25:8191
|
||||
description: "Проходит проверку Cloudflare для RuTracker (на Pi — минуты). Трогать не нужно"
|
||||
siteMonitor: http://flaresolverr:8191
|
||||
@@ -0,0 +1,23 @@
|
||||
# Homepage — стартовая страница медиацентра, http://192.168.10.25:3000
|
||||
title: Домашний медиацентр
|
||||
language: ru
|
||||
theme: dark
|
||||
color: slate
|
||||
headerStyle: clean
|
||||
hideVersion: true
|
||||
statusStyle: dot
|
||||
quicklaunch:
|
||||
searchDescriptions: true
|
||||
layout:
|
||||
Как это работает:
|
||||
style: row
|
||||
columns: 4
|
||||
Смотреть:
|
||||
style: row
|
||||
columns: 2
|
||||
Добавить фильм или сериал:
|
||||
style: row
|
||||
columns: 3
|
||||
Загрузки и служебное:
|
||||
style: row
|
||||
columns: 4
|
||||
@@ -0,0 +1,18 @@
|
||||
- greeting:
|
||||
text_size: xl
|
||||
text: "Логин везде: rus · пароль общий"
|
||||
- resources:
|
||||
label: Малина
|
||||
cpu: true
|
||||
memory: true
|
||||
cputemp: true
|
||||
uptime: true
|
||||
- resources:
|
||||
label: Диск медиа (4 ТБ)
|
||||
disk: /mnt/backup
|
||||
- datetime:
|
||||
text_size: l
|
||||
locale: ru
|
||||
format:
|
||||
dateStyle: long
|
||||
timeStyle: short
|
||||
@@ -3,7 +3,7 @@
|
||||
ничего не ломает, уже сделанное пропускает.
|
||||
|
||||
Запуск на малине: python3 /opt/media/provision.py [шаг ...]
|
||||
Шаги: qbit jellyfin arr prowlarr bazarr seerr (без аргументов — все).
|
||||
Шаги: qbit jellyfin arr prowlarr bazarr seerr connect homepage (без аргументов — все).
|
||||
Пароль админа берётся из /opt/media/.admin-password, API-ключи — из config.xml.
|
||||
"""
|
||||
from http.cookiejar import CookieJar
|
||||
@@ -256,6 +256,58 @@ def prowlarr():
|
||||
log("prowlarr ok")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Homepage + Jellyfin connect
|
||||
def jf_apikey(name):
|
||||
"""API-ключ Jellyfin для приложения name (создаётся при отсутствии)."""
|
||||
tok = jf_token()
|
||||
h = {"Authorization": f'{JF_AUTH}, Token="{tok}"'}
|
||||
find = lambda: next((x["AccessToken"] for x in http("GET", JF + "/Auth/Keys", headers=h)[1]["Items"]
|
||||
if x["AppName"] == name), None)
|
||||
if not find():
|
||||
http("POST", f"{JF}/Auth/Keys?app={name}", headers=h)
|
||||
return find()
|
||||
|
||||
|
||||
def homepage():
|
||||
"""/opt/media/homepage.env (rus 600) — ключи для виджетов, в репо их нет."""
|
||||
import os, yaml
|
||||
env = {
|
||||
"HOMEPAGE_VAR_JELLYFIN_KEY": jf_apikey("homepage"),
|
||||
"HOMEPAGE_VAR_SEERR_KEY": json.load(open(f"{BASE}/config/seerr/settings.json"))["main"]["apiKey"],
|
||||
"HOMEPAGE_VAR_RADARR_KEY": apikey("radarr"),
|
||||
"HOMEPAGE_VAR_SONARR_KEY": apikey("sonarr"),
|
||||
"HOMEPAGE_VAR_PROWLARR_KEY": apikey("prowlarr"),
|
||||
"HOMEPAGE_VAR_BAZARR_KEY": yaml.safe_load(open(f"{BASE}/config/bazarr/config/config.yaml"))["auth"]["apikey"],
|
||||
"HOMEPAGE_VAR_QBIT_USER": USER,
|
||||
"HOMEPAGE_VAR_QBIT_PASS": PW,
|
||||
}
|
||||
old = os.umask(0o077)
|
||||
open(f"{BASE}/homepage.env", "w").write("".join(f"{k}={v}\n" for k, v in env.items()))
|
||||
os.umask(old)
|
||||
log("homepage env written (перезапусти контейнер homepage, если ключи сменились)")
|
||||
|
||||
|
||||
def connect():
|
||||
"""Radarr/Sonarr → Jellyfin: обновить библиотеку сразу после импорта (иначе ждать скан по расписанию)."""
|
||||
k = jf_apikey("arr-connect")
|
||||
for name, a in ARR.items():
|
||||
h = {"X-Api-Key": apikey(name)}
|
||||
u = f"{a['url']}/api/{a['api']}"
|
||||
st, ns = http("GET", u + "/notification", headers=h)
|
||||
if any(n["implementation"] == "MediaBrowser" for n in ns):
|
||||
continue
|
||||
st, schema = http("GET", u + "/notification/schema", headers=h)
|
||||
n = next(x for x in schema if x["implementation"] == "MediaBrowser")
|
||||
vals = {"host": "192.168.10.25", "port": 8096, "useSsl": False, "apiKey": k, "notify": False, "updateLibrary": True}
|
||||
for f in n["fields"]:
|
||||
if f["name"] in vals:
|
||||
f["value"] = vals[f["name"]]
|
||||
n.update({"name": "Jellyfin", "onDownload": True, "onUpgrade": True, "onRename": True,
|
||||
"onMovieDelete": True, "onMovieFileDelete": True, "onSeriesDelete": True, "onEpisodeFileDelete": True})
|
||||
st, r = http("POST", u + "/notification", n, h)
|
||||
log(name, "jellyfin connect", st, r if st >= 300 else "")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Bazarr
|
||||
def bazarr():
|
||||
import yaml # python3-yaml есть в Raspberry Pi OS
|
||||
@@ -333,7 +385,7 @@ def jellyseerr():
|
||||
log("seerr ok")
|
||||
|
||||
|
||||
STEPS = {"qbit": qbit, "jellyfin": jellyfin, "arr": arr, "prowlarr": prowlarr, "bazarr": bazarr, "seerr": jellyseerr}
|
||||
STEPS = {"qbit": qbit, "jellyfin": jellyfin, "arr": arr, "prowlarr": prowlarr, "bazarr": bazarr, "connect": connect, "homepage": homepage, "seerr": jellyseerr}
|
||||
|
||||
if __name__ == "__main__":
|
||||
for s in sys.argv[1:] or list(STEPS):
|
||||
|
||||
Reference in New Issue
Block a user