From 94bc9834014d2d7fd06931963f2340e921516e93 Mon Sep 17 00:00:00 2001 From: gilfanovr Date: Sun, 11 Oct 2026 16:10:52 +0300 Subject: [PATCH] =?UTF-8?q?rpi-media:=20=D1=81=D1=82=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=20Homepage,=20Radarr/Sonarr=20=E2=86=92=20Jellyfin?= =?UTF-8?q?=20connect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - homepage/: сервисы с описанием и живыми виджетами; ключи — в homepage.env на малине - Jellyfin-виджет через customapi (встроенный не совместим с Jellyfin 12) - provision.py: шаги homepage и connect Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- servers/rpi.md | 6 +- stacks/rpi-media/docker-compose.yml | 18 +++++ stacks/rpi-media/homepage/bookmarks.yaml | 1 + stacks/rpi-media/homepage/docker.yaml | 1 + stacks/rpi-media/homepage/services.yaml | 99 ++++++++++++++++++++++++ stacks/rpi-media/homepage/settings.yaml | 23 ++++++ stacks/rpi-media/homepage/widgets.yaml | 18 +++++ stacks/rpi-media/provision.py | 56 +++++++++++++- 9 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 stacks/rpi-media/homepage/bookmarks.yaml create mode 100644 stacks/rpi-media/homepage/docker.yaml create mode 100644 stacks/rpi-media/homepage/services.yaml create mode 100644 stacks/rpi-media/homepage/settings.yaml create mode 100644 stacks/rpi-media/homepage/widgets.yaml diff --git a/AGENTS.md b/AGENTS.md index 199f998..769c4cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` в том же прогоне. Контракт и diff --git a/servers/rpi.md b/servers/rpi.md index 66a5515..acff417 100644 --- a/servers/rpi.md +++ b/servers/rpi.md @@ -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//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 на роутере **не проброшен** — входящих соединений нет, раздача и скорость хуже. diff --git a/stacks/rpi-media/docker-compose.yml b/stacks/rpi-media/docker-compose.yml index 1b8669f..b6d1faf 100644 --- a/stacks/rpi-media/docker-compose.yml +++ b/stacks/rpi-media/docker-compose.yml @@ -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" diff --git a/stacks/rpi-media/homepage/bookmarks.yaml b/stacks/rpi-media/homepage/bookmarks.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/stacks/rpi-media/homepage/bookmarks.yaml @@ -0,0 +1 @@ +[] diff --git a/stacks/rpi-media/homepage/docker.yaml b/stacks/rpi-media/homepage/docker.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/stacks/rpi-media/homepage/docker.yaml @@ -0,0 +1 @@ +{} diff --git a/stacks/rpi-media/homepage/services.yaml b/stacks/rpi-media/homepage/services.yaml new file mode 100644 index 0000000..1bd5720 --- /dev/null +++ b/stacks/rpi-media/homepage/services.yaml @@ -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 diff --git a/stacks/rpi-media/homepage/settings.yaml b/stacks/rpi-media/homepage/settings.yaml new file mode 100644 index 0000000..2f7f7db --- /dev/null +++ b/stacks/rpi-media/homepage/settings.yaml @@ -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 diff --git a/stacks/rpi-media/homepage/widgets.yaml b/stacks/rpi-media/homepage/widgets.yaml new file mode 100644 index 0000000..2e264ab --- /dev/null +++ b/stacks/rpi-media/homepage/widgets.yaml @@ -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 diff --git a/stacks/rpi-media/provision.py b/stacks/rpi-media/provision.py index ae809ad..da6a6f5 100644 --- a/stacks/rpi-media/provision.py +++ b/stacks/rpi-media/provision.py @@ -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):