5 Commits
14 changed files with 190 additions and 169 deletions
+2
View File
@@ -18,6 +18,8 @@ RUN uv sync --frozen --no-dev --no-cache
# ---------------------------------------------------------------------------
FROM python:3.13-slim-bookworm
LABEL org.opencontainers.image.source="https://git.lanro.eu/Roman/ohm_streaming" \
org.opencontainers.image.description="Ohm Stream Downloader — outil technique d'automatisation, ne fournit ni n'héberge aucun contenu. L'usage relève de la seule responsabilité de l'utilisateur (droit d'auteur)."
# ffmpeg (téléchargements HLS), ca-certificates (scraping HTTPS),
# gosu (bascule utilisateur non-root dans l'entrypoint)
RUN apt-get update \
+54 -5
View File
@@ -3,10 +3,34 @@
Application web **auto-hébergée** (homelab) : centre de contrôle unique pour découvrir,
regarder et télécharger des animes et séries VOSTFR/VF.
## ⚖️ Avertissement légal
Ohm Stream Downloader est un **outil technique d'automatisation** : il ne
fournit, n'héberge ni ne distribue **aucun contenu**. Il automatise uniquement
l'accès à des flux déjà publiquement accessibles sur des sites tiers, dont
l'auteur n'est pas responsable et qu'il n'exploite pas.
L'utilisation de ce logiciel relève de la **seule responsabilité de
l'utilisateur**, tenu de respecter la législation de son pays — notamment le
droit d'auteur (en France : art. L.122-4 du Code de la propriété
intellectuelle). Le téléchargement ou la diffusion d'œuvres protégées sans
autorisation est interdit par la loi.
Projet publié à des fins **éducatives et de recherche**. L'auteur décline
toute responsabilité en cas d'usage illégal et n'encourage en aucune façon le
piratage.
> **Disclaimer (EN)**: this project is a technical automation tool. It does
> not provide, host or distribute any content — it merely automates access to
> streams already publicly available on third-party websites that the author
> neither controls nor operates. Users are solely responsible for complying
> with their local laws, including copyright. The author assumes no liability
> for illegal use and does not endorse piracy.
## Déploiement (Docker) — recommandé
Le déploiement officiel passe par Docker Compose : l'image (ffmpeg inclus) est
hébergée sur le **registre privé du Gitea** — rien n'est publié publiquement.
hébergée publiquement sur **GHCR** (`ghcr.io/aulyriusohm/ohm-streaming`) — aucun compte requis.
### Installation guidée (recommandée)
@@ -18,14 +42,13 @@ git clone https://git.lanro.eu/Roman/ohm_streaming.git && cd ohm_streaming
Le script vérifie Docker, demande la **destination des épisodes** (dossier dédié
recommandé — voir « Bibliothèque Plex/Sonarr » ci-dessous), le port, génère les
secrets, branche le montage et démarre. Non interactif aussi :
`./scripts/install.sh --dir /srv/animes --port 8777 --skip-login`.
`./scripts/install.sh --dir /srv/animes --port 8777`.
### À la main
```bash
# 1. Récupérer le projet puis se connecter au registre privé (compte Gitea avec accès lecture)
# 1. Récupérer le projet
git clone https://git.lanro.eu/Roman/ohm_streaming.git && cd ohm_streaming
docker login git.lanro.eu
# 2. Configuration locale
cp .env.example .env
@@ -36,7 +59,33 @@ docker compose up -d
```
Puis ouvrir http://localhost:8777 — le **premier compte créé est administrateur**.
Les données (`data/`, `downloads/`) sont montées en volumes : elles survivent aux
Les données (`data/`, `downloads/`) sont montées en volumes : elles survivent aux mises à jour.
### Variante Podman
Même déploiement via `docker-compose.podman.yml` (socket Podman pour Watchtower,
au lieu du socket Docker) :
```bash
# 0. Socket Podman (une seule fois) — rootless :
systemctl --user enable --now podman.socket
sudo loginctl enable-linger $USER # persister après déconnexion
export PODMAN_SOCKET=/run/user/$(id -u)/podman/podman.sock
# rootful : sudo systemctl enable --now podman.socket (aucun export nécessaire)
# 1. Récupérer le projet
git clone https://git.lanro.eu/Roman/ohm_streaming.git && cd ohm_streaming
# 2. Configuration locale (identique à Docker)
cp .env.example .env # → OHM_SECRET_KEY et WATCHTOWER_TOKEN obligatoires
# 3. Démarrage
podman compose -f docker-compose.podman.yml up -d
# (« podman compose » délègue à docker-compose ; sinon : podman-compose)
```
La mise à jour depuis la page Admin (bouton Watchtower) fonctionne à l'identique ;
à la main : `podman compose -f docker-compose.podman.yml pull && podman compose -f docker-compose.podman.yml up -d`.
### Bibliothèque Plex / Sonarr existante
+4 -1
View File
@@ -34,7 +34,10 @@ class Settings(BaseSettings):
kitsu_base_url: str = "https://kitsu.io/api/edge"
metadata_cache_ttl_hours: int = 72
# Mise à jour (déploiement Docker — Watchtower compagnon)
# Mise à jour (dépôt public — lecture anonyme de l'API Gitea)
gitea_url: str = "https://git.lanro.eu"
gitea_repo: str = "Roman/ohm_streaming"
# Déploiement Docker — Watchtower compagnon
watchtower_url: str = ""
watchtower_token: str = ""
+1 -13
View File
@@ -28,7 +28,6 @@ from app.services.settings import (
set_source_base_url,
set_source_enabled,
set_source_health,
set_update_config,
)
from app.services.sonarr import sonarr
from app.services.update import UpdateError, fetch_latest_version, trigger_update
@@ -222,23 +221,12 @@ async def test_sonarr(admin: AdminUser) -> dict:
# ---------------------------------------------------------------- mise à jour logicielle
class UpdateConfig(BaseModel):
gitea_url: str
repo: str
token: str
@router.get("/update")
async def get_update(admin: AdminUser) -> dict:
"""Version courante, dernière version disponible et configuration Gitea."""
"""Version courante, dernière version disponible et patchnote."""
return await update_status()
@router.put("/update")
async def save_update(payload: UpdateConfig, admin: AdminUser) -> dict:
await set_update_config(payload.gitea_url, payload.repo, payload.token)
return await update_status()
@router.post("/update/check")
async def check_update(admin: AdminUser) -> dict:
"""Force la re-vérification de la dernière version (ignore le cache)."""
-20
View File
@@ -114,23 +114,3 @@ async def set_sonarr_config(url: str, apikey: str) -> None:
await set_setting(SONARR_APIKEY_KEY, apikey.strip())
logger.info("Configuration Sonarr enregistrée (%s)", url)
# ---------------------------------------------------------------- mise à jour logicielle
UPDATE_GITEA_URL_KEY = "update:gitea_url"
UPDATE_REPO_KEY = "update:repo"
UPDATE_TOKEN_KEY = "update:token"
async def get_update_config() -> dict[str, str]:
return {
"gitea_url": await get_setting(UPDATE_GITEA_URL_KEY, "https://git.lanro.eu"),
"repo": await get_setting(UPDATE_REPO_KEY, "Roman/ohm_streaming"),
"token": await get_setting(UPDATE_TOKEN_KEY, ""),
}
async def set_update_config(gitea_url: str, repo: str, token: str) -> None:
await set_setting(UPDATE_GITEA_URL_KEY, gitea_url.rstrip("/"))
await set_setting(UPDATE_REPO_KEY, repo.strip().strip("/"))
await set_setting(UPDATE_TOKEN_KEY, token.strip())
logger.info("Configuration de mise à jour enregistrée (%s)", repo)
+18 -21
View File
@@ -1,6 +1,7 @@
"""Mises à jour logicielles.
- Détection : dernier tag semver du dépôt Gitea via son API (jeton requis, repo privé).
- Détection : dernier tag semver du dépôt Gitea public via son API (lecture anonyme),
avec le message du tag comme patchnote.
- Application : POST à Watchtower (compagnon docker-compose) qui tire la nouvelle
image et recrée le conteneur — quelques secondes d'indisponibilité.
"""
@@ -12,7 +13,6 @@ import time
import httpx
from app.config import get_settings
from app.services.settings import get_update_config
from app.version import get_version
logger = logging.getLogger(__name__)
@@ -20,11 +20,11 @@ logger = logging.getLogger(__name__)
_TAG_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$")
_CACHE_TTL = 300.0 # secondes
_cache: dict[str, object] = {"checked_at": 0.0, "latest": None}
_cache: dict[str, object] = {"checked_at": 0.0, "latest": None, "notes": None}
class UpdateError(Exception):
"""Erreur de mise à jour (config absente, Gitea injoignable, Watchtower KO)."""
"""Erreur de mise à jour (Gitea injoignable, Watchtower KO)."""
def parse_tag(tag: str) -> tuple[int, int, int] | None:
@@ -41,51 +41,48 @@ def is_newer(latest: str, current: str) -> bool:
async def fetch_latest_version(*, force: bool = False) -> str | None:
"""Dernier tag semver du dépôt (cache 5 min). None si non configuré ou erreur."""
"""Dernier tag semver du dépôt + patchnote (cache 5 min). None si erreur."""
now = time.monotonic()
latest_cache = _cache["latest"]
if not force and latest_cache and now - float(_cache["checked_at"]) < _CACHE_TTL:
return str(latest_cache) # type: ignore[arg-type]
config = await get_update_config()
if not config["repo"]:
return None
url = f"{config['gitea_url']}/api/v1/repos/{config['repo']}/tags?limit=20"
# Jeton requis uniquement pour un dépôt privé (public en lecture : inutile)
headers = {"Authorization": f"token {config['token']}"} if config["token"] else {}
settings = get_settings()
url = f"{settings.gitea_url}/api/v1/repos/{settings.gitea_repo}/tags?limit=20"
try:
async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client:
resp = await client.get(url, headers=headers)
resp = await client.get(url)
resp.raise_for_status()
tags = [t["name"] for t in resp.json() if parse_tag(t.get("name", ""))]
tags = [
(t["name"], (t.get("message") or "").strip())
for t in resp.json()
if parse_tag(t.get("name", ""))
]
except (httpx.HTTPError, ValueError, KeyError) as exc:
logger.warning("Vérification de mise à jour impossible : %s", exc)
return None
latest = max(tags, key=parse_tag) if tags else None # type: ignore[arg-type]
_cache.update(checked_at=now, latest=latest)
latest, notes = max(tags, key=lambda t: parse_tag(t[0])) if tags else (None, None)
_cache.update(checked_at=now, latest=latest, notes=notes or None)
if latest and is_newer(latest, get_version()):
logger.info("Nouvelle version disponible : %s (courante %s)", latest, get_version())
return latest
def invalidate_cache() -> None:
_cache.update(checked_at=0.0, latest=None)
_cache.update(checked_at=0.0, latest=None, notes=None)
async def status() -> dict[str, object]:
"""État complet : version courante, dernière dispo, config."""
config = await get_update_config()
"""État complet : version courante, dernière dispo, patchnote."""
current = get_version()
latest = await fetch_latest_version()
return {
"current": current,
"latest": latest,
"notes": _cache["notes"],
"update_available": bool(latest and is_newer(latest, current)),
"configured": bool(config["repo"]),
"docker": bool(get_settings().watchtower_url),
"config": config,
}
+11 -26
View File
@@ -111,28 +111,23 @@
</p>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center">
<input class="input" x-model="upd.config.gitea_url" placeholder="https://git.lanro.eu"
style="flex:2;min-width:220px" @change="upd._dirty = true">
<input class="input" x-model="upd.config.repo" placeholder="Roman/ohm_streaming"
style="flex:1;min-width:180px" @change="upd._dirty = true">
<input class="input" x-model="upd.config.token" type="password" placeholder="Jeton d'accès Gitea"
style="flex:1;min-width:180px" @change="upd._dirty = true">
<button class="btn btn-sm" @click="saveUpdate()" :disabled="!upd._dirty">Enregistrer</button>
<button class="btn btn-sm btn-ghost" @click="checkUpdate()" :disabled="upd._checking"
x-text="upd._checking ? '…' : 'Vérifier'"></button>
<button class="btn btn-sm btn-accent" x-show="upd.update_available" @click="applyUpdate()"
:disabled="upd._applying" x-text="upd._applying ? 'Mise à jour…' : '⬆ Mettre à jour maintenant'"></button>
</div>
<p x-show="!upd.configured" style="margin:0.5rem 0 0;font-size:0.85rem;color:var(--text-dim)">
Renseignez le dépôt Gitea pour activer la détection des mises à jour.
Jeton d'accès (droit « lecture ») nécessaire uniquement si le dépôt est privé.
</p>
<p x-show="upd.configured && !upd.docker" style="margin:0.5rem 0 0;font-size:0.85rem;color:var(--text-dim)">
<details x-show="upd.notes" style="margin:0.6rem 0 0">
<summary style="cursor:pointer;font-size:0.85rem;color:var(--text-dim)">
Patchnote <span x-text="upd.latest"></span>
</summary>
<pre style="white-space:pre-wrap;font-size:0.82rem;margin:0.5rem 0 0;color:var(--text-dim)"
x-text="upd.notes"></pre>
</details>
<p x-show="!upd.docker" style="margin:0.5rem 0 0;font-size:0.85rem;color:var(--text-dim)">
⚠ Mise à jour automatique disponible uniquement en déploiement Docker
(<code>docker compose pull &amp;&amp; docker compose up -d</code> sinon).
</p>
</div>
+
<div class="panel">
<h2>👥 Utilisateurs</h2>
@@ -171,9 +166,8 @@ function adminPage() {
return {
users: [], sources: [], stats: {}, forbidden: false,
upd: {
current: '', latest: null, update_available: false, configured: false, docker: false,
config: { gitea_url: '', repo: '', token: '' },
_dirty: false, _checking: false, _applying: false,
current: '', latest: null, notes: null, update_available: false, docker: false,
_checking: false, _applying: false,
},
integrations: {
torznab: { apikey: '', endpoint: '' }, sonarr: { url: '', apikey: '' },
@@ -287,18 +281,9 @@ function adminPage() {
applyUpdateData(data) {
this.upd.current = data.current;
this.upd.latest = data.latest;
this.upd.notes = data.notes;
this.upd.update_available = data.update_available;
this.upd.configured = data.configured;
this.upd.docker = data.docker;
this.upd.config = { ...data.config, _dirty: false };
},
async saveUpdate() {
const res = await fetch('/api/admin/update', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.upd.config),
});
if (res.ok) { this.applyUpdateData(await res.json()); toast('✔ Configuration enregistrée'); }
},
async checkUpdate() {
+51
View File
@@ -0,0 +1,51 @@
# Déploiement OhmStreaming sous Podman — variante de docker-compose.yml
#
# 1. Activer le socket Podman (une seule fois) :
# rootless : systemctl --user enable --now podman.socket
# sudo loginctl enable-linger $USER # persister après déconnexion
# export PODMAN_SOCKET=/run/user/$(id -u)/podman/podman.sock
# rootful : sudo systemctl enable --now podman.socket
# 2. cp .env.example .env && $EDITOR .env (OHM_SECRET_KEY + WATCHTOWER_TOKEN obligatoires)
# 3. podman compose -f docker-compose.podman.yml up -d
# (ou podman-compose -f docker-compose.podman.yml up -d)
#
# Mise à jour : page Admin → « Mise à jour », ou manuellement :
# podman compose -f docker-compose.podman.yml pull && podman compose -f docker-compose.podman.yml up -d
services:
ohm:
image: ${OHM_IMAGE:-ghcr.io/aulyriusohm/ohm-streaming:latest}
restart: unless-stopped
ports:
- "${OHM_PORT:-8777}:8777"
env_file: .env
environment:
# Chemins internes (volumes ci-dessous) — ne pas changer
OHM_DATA_DIR: /data
OHM_DOWNLOAD_DIR: /downloads
OHM_DATABASE_PATH: /data/ohm.db
# Watchtower compagnon sur le réseau interne compose
OHM_WATCHTOWER_URL: http://watchtower:8080
OHM_WATCHTOWER_TOKEN: ${WATCHTOWER_TOKEN:?WATCHTOWER_TOKEN manquant — voir .env.example}
volumes:
# Fedora/SELinux : suffixer de « :z » en cas d'erreur « Permission denied »
- ./data:/data
- ./downloads:/downloads
# Compagnon de mise à jour : recrée le conteneur ohm quand l'admin le demande
# (API HTTP interne uniquement, aucun port publié sur l'hôte)
watchtower:
image: containrrr/watchtower
restart: unless-stopped
volumes:
# Socket Podman (compatible API Docker) au lieu de /var/run/docker.sock
- ${PODMAN_SOCKET:-/run/podman/podman.sock}:/var/run/docker.sock
environment:
# Pas de vérification périodique : uniquement sur appel de l'admin
WATCHTOWER_HTTP_API_UPDATE: "true"
WATCHTOWER_HTTP_API_TOKEN: ${WATCHTOWER_TOKEN:?WATCHTOWER_TOKEN manquant — voir .env.example}
# Ne mettre à jour que les conteneurs étiquetés (ohm), pas watchtower lui-même
WATCHTOWER_LABEL_ENABLE: "true"
# Pas de DOCKER_API_VERSION : Podman annonce lui-même la version d'API
# Supprimer les anciennes images après mise à jour
WATCHTOWER_CLEANUP: "true"
+6 -4
View File
@@ -2,16 +2,18 @@
#
# Installation guidée (recommandée) : ./scripts/install.sh
# Ou à la main :
# 1. docker login git.lanro.eu (compte Gitea avec accès lecture au repo)
# 2. cp .env.example .env && $EDITOR .env (OHM_SECRET_KEY + WATCHTOWER_TOKEN obligatoires)
# 3. docker compose up -d
# 1. cp .env.example .env && $EDITOR .env (OHM_SECRET_KEY + WATCHTOWER_TOKEN obligatoires)
# 2. docker compose up -d
#
# Image par défaut : GHCR (publique). Pour utiliser le registre Gitea à la place :
# OHM_IMAGE=git.lanro.eu/roman/ohm_streaming:latest dans .env (+ docker login git.lanro.eu)
#
# Mise à jour : page Admin → « Mise à jour », ou manuellement :
# docker compose pull && docker compose up -d
services:
ohm:
image: git.lanro.eu/roman/ohm_streaming:latest
image: ${OHM_IMAGE:-ghcr.io/aulyriusohm/ohm-streaming:latest}
# Build local (dev) : décommenter ces lignes et commenter « image: » ci-dessus
# build:
# context: .
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "ohm-stream"
version = "0.1.0"
version = "0.2.0"
description = "Ohm Stream Downloader — centre de contrôle auto-hébergé pour animes et séries VOSTFR"
requires-python = ">=3.13"
dependencies = [
+2 -18
View File
@@ -5,7 +5,7 @@
# bash scripts/install.sh (depuis un clone du dépôt)
# curl -fsSL <gitea>/raw/branch/main/scripts/install.sh | bash
#
# Options non interactives : --dir <chemin> --port <n> --skip-login
# Options non interactives : --dir <chemin> --port <n>
# (les valeurs passées en option court-circuitent les questions)
#
# Pose les questions (destination des animés, port), génère les secrets,
@@ -23,12 +23,11 @@ ok() { printf " ${GREEN}✔${RESET} %s\n" "$1"; }
warn() { printf " ${YELLOW}⚠${RESET} %s\n" "$1"; }
die() { printf " ${RED}✖ %s${RESET}\n" "$1" >&2; exit 1; }
ARG_DIR="" ARG_PORT="" ARG_SKIP_LOGIN=0
ARG_DIR="" ARG_PORT=""
while [ $# -gt 0 ]; do
case "$1" in
--dir) ARG_DIR="$2"; shift 2;;
--port) ARG_PORT="$2"; shift 2;;
--skip-login) ARG_SKIP_LOGIN=1; shift;;
*) shift;;
esac
done
@@ -74,21 +73,6 @@ if [ ! -f "$COMPOSE_FILE" ]; then
fi
ok "Projet prêt dans $(pwd)"
# ---------------------------------------------------------------- registre privé
step "Accès au registre privé ($REGISTRY_HOST)"
if [ "$ARG_SKIP_LOGIN" = "1" ]; then
warn "--skip-login : connexion au registre ignorée"
elif docker login "$REGISTRY_HOST" 2>/dev/null; then
ok "Connecté au registre"
else
warn "Pas encore connecté — un compte Gitea avec accès lecture au dépôt est requis."
if [ "$(ask "Se connecter maintenant (docker login) ?" "o")" = "o" ]; then
docker login "$REGISTRY_HOST" || die "docker login a échoué — relance le script après t'être connecté."
ok "Connecté au registre"
else
die "Sans docker login, l'image ne pourra pas être tirée."
fi
fi
# ---------------------------------------------------------------- destination des épisodes
step "Destination des épisodes téléchargés"
Executable → Regular
+12 -9
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env bash
# Publie une version d'OhmStreaming :
# bump de version → commit + tag git → build de l'image Docker → push registre Gitea.
# bump de version → commit + tag git → build de l'image Docker → push GHCR (public) + Gitea.
#
# Usage : ./scripts/release.sh 0.2.0
# Usage : ./scripts/release.sh 0.2.0 ["patchnote multi-lignes"]
# Option : OHM_REGISTRY=git.lanro.eu/roman/ohm_streaming (défaut) pour viser un autre registre.
set -euo pipefail
VERSION="${1:?Usage : ./scripts/release.sh <version> (ex. 0.2.0)}"
VERSION="${1:?Usage : ./scripts/release.sh <version> [patchnote] (ex : 0.2.0)}"
REGISTRY="${OHM_REGISTRY:-git.lanro.eu/roman/ohm_streaming}"
GHCR="${OHM_GHCR:-ghcr.io/aulyriusohm/ohm-streaming}"
cd "$(dirname "$0")/.."
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "✖ « $VERSION » n'est pas un semver X.Y.Z"; exit 1; }
@@ -30,15 +31,17 @@ git add pyproject.toml uv.lock
git commit -m "v$VERSION"
echo "▶ 2/3 Tag et push git"
git tag "$TAG"
git tag -a "$TAG" -m "${2:-$TAG}"
git push origin HEAD
git push origin "$TAG"
echo "▶ 3/3 Image Docker → $REGISTRY"
docker build --build-arg VERSION="$VERSION" -t "$REGISTRY:$VERSION" -t "$REGISTRY:latest" .
docker push "$REGISTRY:$VERSION"
docker push "$REGISTRY:latest"
echo "▶ 3/3 Image Docker → $GHCR (public) + $REGISTRY (Gitea)"
docker build --build-arg VERSION="$VERSION" -t "$GHCR:$VERSION" -t "$GHCR:latest" .
docker push "$GHCR:$VERSION" && docker push "$GHCR:latest"
docker tag "$GHCR:$VERSION" "$REGISTRY:$VERSION"
docker tag "$GHCR:latest" "$REGISTRY:latest"
docker push "$REGISTRY:$VERSION" && docker push "$REGISTRY:latest"
echo
echo "✔ v$VERSION publiée ($REGISTRY:$VERSION et :latest)"
echo "✔ v$VERSION publiée ($GHCR:$VERSION et :latest — public, miroir Gitea $REGISTRY)"
echo " Les instances la détecteront via Admin → Mise à jour → « Vérifier »."
+27 -50
View File
@@ -3,8 +3,8 @@
import httpx
import pytest
from app.config import get_settings
from app.services import update as update_service
from app.services.settings import get_update_config
from app.version import get_version
@@ -15,6 +15,12 @@ def reset_update_cache():
update_service.invalidate_cache()
def _patch_gitea(monkeypatch, url: str = "https://git.example", repo: str = "roman/ohm") -> None:
settings = get_settings()
monkeypatch.setattr(settings, "gitea_url", url)
monkeypatch.setattr(settings, "gitea_repo", repo)
# ---------------------------------------------------------------- semver / version
@@ -41,18 +47,9 @@ def test_get_version_env_override(monkeypatch):
# ---------------------------------------------------------------- détection Gitea
async def test_fetch_latest_without_repo():
from app.services.settings import set_update_config
await set_update_config("https://git.example", "", "tok")
assert await update_service.fetch_latest_version(force=True) is None
async def test_fetch_latest_public_repo_no_token(monkeypatch):
"""Dépôt public : pas de jeton → appel sans en-tête Authorization."""
from app.services.settings import set_update_config
await set_update_config("https://git.example", "roman/ohm", "")
async def test_fetch_latest_no_auth_header(monkeypatch):
"""Dépôt public : appel sans en-tête Authorization."""
_patch_gitea(monkeypatch)
class FakeResponse:
def raise_for_status(self) -> None:
@@ -73,17 +70,20 @@ async def test_fetch_latest_public_repo_no_token(monkeypatch):
async def test_fetch_latest_picks_highest_semver(monkeypatch):
"""Avec jeton : en-tête Authorization + plus haut semver retenu, puis cache."""
from app.services.settings import set_update_config
await set_update_config("https://git.example", "roman/ohm", "tok")
"""Plus haut semver retenu + patchnote du tag, puis cache."""
_patch_gitea(monkeypatch)
class FakeResponse:
def raise_for_status(self) -> None:
pass
def json(self) -> list[dict]:
return [{"name": "v0.1.0"}, {"name": "v0.2.3"}, {"name": "v1.0.0-rc"}, {"name": "divers"}]
return [
{"name": "v0.1.0", "message": "ancien"},
{"name": "v0.2.3", "message": "correctifs\n"},
{"name": "v1.0.0-rc"},
{"name": "divers"},
]
calls: list[tuple[str, dict]] = []
@@ -92,21 +92,18 @@ async def test_fetch_latest_picks_highest_semver(monkeypatch):
return FakeResponse()
monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
latest = await update_service.fetch_latest_version(force=True)
status = await update_service.status()
assert latest == "v0.2.3"
assert calls == [
("https://git.example/api/v1/repos/roman/ohm/tags?limit=20", {"Authorization": "token tok"})
]
assert status["latest"] == "v0.2.3"
assert status["notes"] == "correctifs"
assert calls == [("https://git.example/api/v1/repos/roman/ohm/tags?limit=20", {})]
# puis servi par le cache (plus d'appel réseau)
assert await update_service.fetch_latest_version() == "v0.2.3"
assert len(calls) == 1
async def test_fetch_latest_network_error_degrades(monkeypatch):
from app.services.settings import set_update_config
await set_update_config("https://git.example", "roman/ohm", "tok")
_patch_gitea(monkeypatch)
async def fake_get(self: httpx.AsyncClient, url: str, headers: dict | None = None):
raise httpx.ConnectError("injoignable")
@@ -148,33 +145,13 @@ async def test_update_status_defaults(client, admin_cookies, monkeypatch):
r = await client.get("/api/admin/update", cookies=admin_cookies)
assert r.status_code == 200
data = r.json()
assert data["configured"] is True and data["docker"] is False # repo par défaut présent
assert data["latest"] is None and data["update_available"] is False
assert data["config"]["gitea_url"] == "https://git.lanro.eu"
assert data["config"]["repo"] == "Roman/ohm_streaming"
assert data["docker"] is False and data["notes"] is None
async def test_update_save_config_normalizes(client, admin_cookies):
r = await client.put(
"/api/admin/update",
json={"gitea_url": "https://git.example/", "repo": "/roman/ohm/", "token": " tok "},
cookies=admin_cookies,
)
assert r.status_code == 200
assert await get_update_config() == {
"gitea_url": "https://git.example",
"repo": "roman/ohm",
"token": "tok",
}
async def test_update_check_degrades_gracefully(client, admin_cookies):
# Gitea configuré mais injoignable : le check répond quand même (latest=None)
await client.put(
"/api/admin/update",
json={"gitea_url": "https://git.inexistant", "repo": "roman/ohm", "token": "tok"},
cookies=admin_cookies,
)
async def test_update_check_degrades_gracefully(client, admin_cookies, monkeypatch):
# Gitea injoignable : le check répond quand même (latest=None)
_patch_gitea(monkeypatch, url="https://git.inexistant", repo="roman/ohm")
r = await client.post("/api/admin/update/check", cookies=admin_cookies)
assert r.status_code == 200 and r.json()["latest"] is None
Generated
+1 -1
View File
@@ -774,7 +774,7 @@ wheels = [
[[package]]
name = "ohm-stream"
version = "0.1.0"
version = "0.2.0"
source = { virtual = "." }
dependencies = [
{ name = "aiosqlite" },