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
b915a58ad9
commit
e02a9a8f1b
Binary file not shown.
@@ -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