Migrate from Poetry and pyenv to uv

This commit is contained in:
2026-10-12 15:48:26 +02:00
parent c399a6aed3
commit c69f5f9166
93 changed files with 13068 additions and 2401 deletions
+23
View File
@@ -0,0 +1,23 @@
[Unit]
Description=HTTP server pro HLS složku Makov webkamery (pro Jellyfin M3U tuner)
After=network-online.target makov-cam.service
Wants=network-online.target
BindsTo=makov-cam.service
[Service]
Type=simple
# WorkingDirectory je rodič hls/, aby šel přes HTTP servírovat i makov.m3u
# (playlist pro Jellyfin), ne jen samotné HLS segmenty.
WorkingDirectory=/var/lib/jellyfin-iptv
# Bind na IP brány docker sítě, na které běží Jellyfin kontejner (ne na
# 127.0.0.1 hostitele — to by z kontejneru bylo jeho vlastní loopback, ne
# hostitelův). Konkrétní hodnota je specifická pro tenhle server, viz
# setup.md.
ExecStart=/usr/bin/python3 -m http.server 8888 --bind 172.19.0.1
Restart=always
RestartSec=5
User=makov-cam
Group=makov-cam
[Install]
WantedBy=multi-user.target
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=Restream Makov webkamery (Krmítko) do lokálního HLS pro Jellyfin
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/makov-cam.sh
Restart=always
RestartSec=5
# Necháme běžet pod samostatným účtem, ne pod root.
User=makov-cam
Group=makov-cam
# Pokud je yt-dlp nainstalované přes snap, ignoruje $HOME nastavené tady —
# zjišťuje domovský adresář přímo z /etc/passwd. Účet je --no-create-home,
# takže je potřeba /home/makov-cam vytvořit a vlastnit ručně (viz setup.md).
# Skript si sám zakládá /var/lib/jellyfin-iptv/hls — účet tam musí mít zápis
# (viz setup.md, sekce "Uživatel a oprávnění").
[Install]
WantedBy=multi-user.target
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Restreams the Makov webcam (YouTube live "Krmítko") into a local HLS
# playlist that never goes stale for the viewer — Jellyfin always points at
# this fixed local path, not at YouTube's own signed/expiring URL.
#
# When ffmpeg dies (the YouTube URL expired, network hiccup, stream briefly
# ended, ...) this script simply exits; systemd (Restart=always) starts it
# again, which resolves a *fresh* URL via yt-dlp. Nothing needs to be done
# by hand.
set -euo pipefail
YOUTUBE_URL="https://www.youtube.com/watch?v=F0fsV1GGZ3M"
OUT_DIR="/var/lib/jellyfin-iptv/hls"
OUT_FILE="${OUT_DIR}/makov.m3u8"
mkdir -p "$OUT_DIR"
# This live stream only serves separate video-only/audio-only HLS renditions
# (no combined progressive format), so we resolve both and let ffmpeg mux
# them. "bestvideo+bestaudio" makes yt-dlp -g print exactly two URLs, video
# then audio; fall back to "best" in case a progressive format reappears.
mapfile -t STREAM_URLS < <(yt-dlp -g -f 'bestvideo+bestaudio/best' "$YOUTUBE_URL")
VIDEO_URL="${STREAM_URLS[0]:-}"
AUDIO_URL="${STREAM_URLS[1]:-}"
if [ -z "$VIDEO_URL" ]; then
echo "yt-dlp nevrátilo žádnou URL (stream offline, nebo se YouTube brání) — končím, systemd to za chvíli zkusí znovu." >&2
exit 1
fi
if [ -z "$AUDIO_URL" ]; then
# Progressive fallback matched: single URL carries both video and audio.
exec ffmpeg -nostdin -loglevel warning \
-re -i "$VIDEO_URL" \
-c copy \
-f hls -hls_time 4 -hls_list_size 6 \
-hls_flags delete_segments+omit_endlist \
-hls_segment_filename "${OUT_DIR}/makov_%03d.ts" \
"$OUT_FILE"
fi
exec ffmpeg -nostdin -loglevel warning \
-re -i "$VIDEO_URL" -re -i "$AUDIO_URL" \
-map 0:v -map 1:a \
-c copy \
-f hls -hls_time 4 -hls_list_size 6 \
-hls_flags delete_segments+omit_endlist \
-hls_segment_filename "${OUT_DIR}/makov_%03d.ts" \
"$OUT_FILE"
+175
View File
@@ -0,0 +1,175 @@
# Makov webkamera → Jellyfin (Live TV)
Restreamuje YouTube live přenos ("Krmítko", https://www.youtube.com/watch?v=F0fsV1GGZ3M)
do lokálního HLS, na který se Jellyfin dívá přes stabilní adresu. Když YouTube
odkaz vyprší nebo spadne, systemd proces sám restartuje a natáhne novou URL —
divák/Jellyfin si ničeho nevšimne.
## 0) Kam skript patří
Toto **musí běžet na stejném stroji (resp. za stejnou veřejnou IP), jako
Jellyfin** — YouTube vrací streamovací URL svázanou s IP adresou, ze které
byla vyžádána (`yt-dlp -g`). Pokud poběží jinde než Jellyfin, `ffmpeg` dostane
403 a nikdy se nerozjede.
## 1) Závislosti
```bash
sudo apt install -y ffmpeg python3
sudo pip install -U yt-dlp # nebo pipx install yt-dlp — apt verze bývá stará
# a YouTube ji rád rozbije
```
Ověř, že `yt-dlp` a `ffmpeg` jedou z shellu bez chyby:
```bash
yt-dlp --version
ffmpeg -version
```
## 2) Uživatel a oprávnění
Vlastní, neprivilegovaný systémový účet — obě služby pod ním běží:
```bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin makov-cam
sudo mkdir -p /var/lib/jellyfin-iptv/hls
sudo chown -R makov-cam:makov-cam /var/lib/jellyfin-iptv
```
Pokud je `yt-dlp` nainstalované přes **snap** (`which yt-dlp` ukazuje
`/snap/bin/yt-dlp`), potřebuje při startu zapisovatelný domovský adresář —
a to i přes `--no-create-home`, protože domov si zjišťuje přímo z
`/etc/passwd`, ne z proměnné `$HOME` (tu přenastavenou v service souboru
ignoruje). Bez toho skončí na `Permission denied: mkdir /home/makov-cam`.
Vytvoř mu ho ručně:
```bash
sudo mkdir -p /home/makov-cam
sudo chown makov-cam:makov-cam /home/makov-cam
sudo chmod 750 /home/makov-cam
```
(Pokud `yt-dlp` instaluješ přes `pip`/`pipx` dle kroku 1, tenhle problém
nenastává a krok lze přeskočit.)
## 3) Nasazení souborů
```bash
sudo install -m 755 makov-cam.sh /usr/local/bin/makov-cam.sh
sudo install -m 644 makov-cam.service /etc/systemd/system/
sudo install -m 644 makov-cam-http.service /etc/systemd/system/
sudo systemctl daemon-reload
```
## 4) Spuštění
```bash
sudo systemctl enable --now makov-cam.service
sudo systemctl enable --now makov-cam-http.service
```
Ověření:
```bash
systemctl status makov-cam.service makov-cam-http.service
journalctl -u makov-cam.service -f # živě sleduj log ffmpeg/yt-dlp
# do pár vteřin by se měl objevit playlist a segmenty:
ls -la /var/lib/jellyfin-iptv/hls/
# a mělo by jít stáhnout přes HTTP:
curl -sI http://127.0.0.1:8888/hls/makov.m3u8
```
`makov-cam-http.service` je svázaná (`BindsTo=`) na `makov-cam.service` — když
restream spadne/restartuje, HTTP server padne/naskočí s ním, ať nikdy
neservíruje mrtvý/prázdný playlist. Pozor: `BindsTo=` jen zajišťuje, že HTTP
server *spadne*, když spadne hlavní služba — nezajišťuje, že se s ní i sám
*nastartuje*. Po `enable --now` obou služeb to není problém (obě se spustí),
ale pokud jen restartuješ `makov-cam.service` ručně, zkontroluj/restartuj i
`makov-cam-http.service`.
HTTP server poslouchá jen na jedné IP (`--bind`), ne na `0.0.0.0` — nemá cenu
ho vystavovat celé síti, stačí odkud se na něj bude dívat Jellyfin:
- **Jellyfin běží přímo na hostu** (ne v kontejneru): `--bind 127.0.0.1`
stačí.
- **Jellyfin běží v Docker kontejneru na tomtéž hostu** (běžný případ) —
`127.0.0.1` uvnitř kontejneru je jeho *vlastní* loopback, ne hostitelův,
takže na hostitelův HTTP server se tak nedostane. Zjisti gateway IP té
docker sítě, na které kontejner běží, a naviaž se na ni:
```bash
docker inspect <jellyfin-container> --format '{{.HostConfig.NetworkMode}}'
docker inspect <jellyfin-container> --format '{{json .NetworkSettings.Networks}}'
# z výstupu vezmi "Gateway", např. 172.19.0.1 — tam se z kontejneru dostaneš
```
a použij `--bind <ta-gateway-IP>` (viz `makov-cam-http.service`). Pokud by
Jellyfin běžel v jiné VM, změň `--bind` na IP, na které je pro ni
dosažitelný, a případně otevři firewall jen pro tenhle port a jen z její
IP.
## 5) Playlist pro Jellyfin
`WorkingDirectory` HTTP služby je `/var/lib/jellyfin-iptv` (rodič `hls/`), aby
šel přes stejný server servírovat i samotný `.m3u` playlist — to se hodí,
pokud Jellyfin (typicky v kontejneru) nevidí souborový systém hostitele a
tuner tak musí načíst playlist přes HTTP URL, ne lokální cestou.
Vytvoř `/var/lib/jellyfin-iptv/makov.m3u`:
```
#EXTM3U
#EXTINF:-1 tvg-id="makov-krmitko" tvg-name="Makov - Krmítko",Makov - Krmítko
http://<BIND_IP>:8888/hls/makov.m3u8
```
kde `<BIND_IP>` je stejná adresa, na kterou je navázaný `makov-cam-http.service`
(viz krok 4).
## 6) Nastavení v Jellyfinu
1. **Dashboard → Live TV → Tuner Devices → +** (Add)
2. Typ: **M3U Tuner**
3. File or URL:
- Jellyfin na hostu s přístupem k souborovému systému: lokální cesta
`/var/lib/jellyfin-iptv/makov.m3u`.
- Jellyfin v kontejneru (nevidí hostitelův FS): HTTP URL
`http://<BIND_IP>:8888/makov.m3u` (viz krok 5).
4. **Save** → Jellyfin načte kanál "Makov - Krmítko" do Live TV
5. Doporučeno: **Dashboard → Live TV → Guide Providers** — pro tenhle kanál
žádného není třeba (žádný EPG), Jellyfin ho ukáže jako kanál bez programu,
což nevadí.
6. Zkontroluj přehrávání v **Live TV** sekci hlavního rozhraní.
Protože kanál pořád ukazuje na stejnou lokální adresu, odpadá starost o
obnovování guide dat nebo expiraci URL na Jellyfin straně — o to se stará jen
`makov-cam.service` na pozadí.
## 7) Autostart po restartu serveru
`enable` v kroku 4 už zajistil start při bootu. Rychlá kontrola:
```bash
systemctl is-enabled makov-cam.service makov-cam-http.service
```
## Řešení problémů
- **`ffmpeg` pořád padá / v logu 403 od googlevideo.com** — nejčastěji IP
mismatch (viz bod 0): stroj, co spouští `yt-dlp -g`/`ffmpeg`, musí mít
stejnou veřejnou IP jako to, odkud by se stream normálně přehrával.
Zkontroluj `journalctl -u makov-cam.service -n 50`.
- **`yt-dlp: command not found` v service logu** — systemd nepoužívá tvůj
interaktivní `$PATH`; ověř `which yt-dlp` a případně uprav `ExecStart` v
`makov-cam.sh`/skriptu na plnou cestu (`/usr/local/bin/yt-dlp` apod.), nebo
nainstaluj přes `sudo pip install`, aby šel do `/usr/local/bin`.
- **Playlist v Jellyfinu jede, ale sekundy/minuty výpadku při refreshi** —
normální, `hls_list_size 6` × `hls_time 4s` drží ~24s okno; při restartu
streamu je pár vteřin díra, než naběhnou nové segmenty. Jde zkrátit/prodloužit
úpravou těch dvou hodnot ve skriptu.
- **YouTube někdy dočasně rozbije `yt-dlp` novou verzí přehrávače** — drž
`yt-dlp` aktuální (`sudo pip install -U yt-dlp`, klidně jako týdenní cron).
- **`ERROR: Requested format is not available`** — stream v tu chvíli nenabízí
žádný kombinovaný (progresivní) formát, jen oddělené video-only/audio-only
HLS řady (ověř přes `yt-dlp -F <url>`). Skript proto primárně používá
`-f 'bestvideo+bestaudio/best'` a při dvou vrácených URL pustí `ffmpeg` se
dvěma `-i` vstupy (`-map 0:v -map 1:a`); jedna URL = fallback na progresivní
formát, pokud se zase objeví.