Migrate from Poetry and pyenv to uv
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
Executable
+49
@@ -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"
|
||||
@@ -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í.
|
||||
Reference in New Issue
Block a user