3 Commits
Author SHA1 Message Date
Roman 6d15fce516 v0.3.0 — Bibliothèque organisée : sections Animés/Séries, groupes repliables par série et dossier
- /api/library enrichi : media_type (domaine source), série/saison/épisode (parsing du titre), dossier
- Filtrage selon la préférence de contenu utilisateur (anime / serie / both)
- Page bibliothèque : sections Animés / Séries & Films, groupes repliables (nb épisodes, dossier, progression, taille)
- neighbors() réutilise le parseur commun (comparaison série + saison)
2026-09-26 14:52:09 +00:00
Roman f79c0cc86e Distribution publique GHCR (miroir, aucun compte requis) + avertissement légal dans le README et le label OCI 2026-09-25 16:36:03 +00:00
Roman 00aae109b0 Déploiement Podman : variante compose (socket podman pour Watchtower) + doc 2026-09-25 16:01:03 +00:00
10 changed files with 301 additions and 63 deletions
+2
View File
@@ -18,6 +18,8 @@ RUN uv sync --frozen --no-dev --no-cache
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
FROM python:3.13-slim-bookworm 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), # ffmpeg (téléchargements HLS), ca-certificates (scraping HTTPS),
# gosu (bascule utilisateur non-root dans l'entrypoint) # gosu (bascule utilisateur non-root dans l'entrypoint)
RUN apt-get update \ 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, 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. 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é ## Déploiement (Docker) — recommandé
Le déploiement officiel passe par Docker Compose : l'image (ffmpeg inclus) est 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) ### 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é 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 recommandé — voir « Bibliothèque Plex/Sonarr » ci-dessous), le port, génère les
secrets, branche le montage et démarre. Non interactif aussi : 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 ### À la main
```bash ```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 git clone https://git.lanro.eu/Roman/ohm_streaming.git && cd ohm_streaming
docker login git.lanro.eu
# 2. Configuration locale # 2. Configuration locale
cp .env.example .env cp .env.example .env
@@ -36,7 +59,33 @@ docker compose up -d
``` ```
Puis ouvrir http://localhost:8777 — le **premier compte créé est administrateur**. 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 ### Bibliothèque Plex / Sonarr existante
+66 -14
View File
@@ -4,6 +4,8 @@ import logging
import mimetypes import mimetypes
import re import re
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from pathlib import PurePosixPath
from urllib.parse import urlparse
import aiosqlite import aiosqlite
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
@@ -24,42 +26,92 @@ CHUNK_SIZE = 1 << 20 # 1 Mio
# ---------------------------------------------------------------- bibliothèque # ---------------------------------------------------------------- bibliothèque
_SEASON_EP_RE = re.compile(r"\bs(\d{1,2})\s*e(\d{1,4}(?:\.\d(?!\d))?)", re.IGNORECASE)
_SEASON_RE = re.compile(r"\b(?:saison|season|s)\s*0?(\d+)", re.IGNORECASE)
_EPISODE_RE = re.compile(r"\b(?:épisode|episode|ep|e)\s*0?(\d+(?:\.\d(?!\d))?)", re.IGNORECASE)
_VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".webm", ".mov", ".m4v", ".ts"}
def parse_title(title: str) -> dict:
"""Extrait nom de série, saison et épisode depuis un titre de téléchargement."""
stem, dot, ext = title.rpartition(".")
if dot and f".{ext.lower()}" in _VIDEO_EXTS:
title = stem
season = episode = None
cut = len(title)
if match := _SEASON_EP_RE.search(title): # format compact S02E05
season, episode = int(match.group(1)), float(match.group(2))
cut = match.start()
else:
if match := _SEASON_RE.search(title):
season = int(match.group(1))
cut = min(cut, match.start())
if match := _EPISODE_RE.search(title):
episode = float(match.group(1))
cut = min(cut, match.start())
series = re.sub(r"\s+", " ", title[:cut].replace(".", " ").replace("_", " ")).strip(" -–—:")
return {"series": series or title, "season": season, "episode": episode}
def classify_media(page_url: str | None, video_url: str | None) -> str:
"""'anime' ou 'serie' selon le domaine de la page/URL source (défaut : anime)."""
from app.scrapers.base import all_sources
hosts = [
urlparse(u).netloc.lower()
for u in (page_url, video_url)
if u and urlparse(u).netloc
]
for src in all_sources():
domain = urlparse(src.base_url).netloc.lower()
if domain and any(h == domain or h.endswith(f".{domain}") for h in hosts):
return "anime" if "anime" in src.media_types else "serie"
return "anime"
@router.get("/library") @router.get("/library")
async def library(user: CurrentUser) -> list[dict]: async def library(user: CurrentUser) -> list[dict]:
"""Fichiers téléchargés, streamables, avec progression de visionnage.""" """Fichiers téléchargés, streamables, enrichis (type, série, dossier) pour le regroupement."""
rows = await db.fetchall( rows = await db.fetchall(
"SELECT d.*, wp.position_seconds FROM downloads d " "SELECT d.*, wp.position_seconds FROM downloads d "
"LEFT JOIN watch_progress wp ON wp.download_id = d.id AND wp.user_id = ? " "LEFT JOIN watch_progress wp ON wp.download_id = d.id AND wp.user_id = ? "
"WHERE d.status = 'done' ORDER BY d.updated_at DESC", "WHERE d.status = 'done' ORDER BY d.updated_at DESC",
(user.id,), (user.id,),
) )
return [dict(row) for row in rows] items = []
for row in rows:
item = dict(row)
item["media_type"] = classify_media(row["page_url"], row["video_url"])
item.update(parse_title(row["title"]))
folder = str(PurePosixPath(row["file_path"]).parent) if row["file_path"] else ""
item["folder"] = "" if folder == "." else folder
items.append(item)
if user.content_preference in ("anime", "serie"):
items = [i for i in items if i["media_type"] == user.content_preference]
return items
@router.get("/library/{download_id}/neighbors") @router.get("/library/{download_id}/neighbors")
async def neighbors(download_id: int) -> dict: async def neighbors(download_id: int) -> dict:
"""Épisode précédent/suivant : heuristique sur les titres (même série, N±1).""" """Épisode précédent/suivant : heuristique sur les titres (même série, N±1)."""
rows = await db.fetchall("SELECT id, title FROM downloads WHERE status = 'done' ORDER BY title") rows = await db.fetchall("SELECT id, title FROM downloads WHERE status = 'done' ORDER BY title")
def parse(title: str) -> tuple[str, float | None]:
match = re.search(r"(?:épisode|episode|ep|e)\s*(\d+(?:\.\d+)?)", title, re.IGNORECASE)
if not match:
return title, None
return title[: match.start()].strip(), float(match.group(1))
current = await db.fetchone("SELECT id, title FROM downloads WHERE id = ?", (download_id,)) current = await db.fetchone("SELECT id, title FROM downloads WHERE id = ?", (download_id,))
if current is None: if current is None:
raise HTTPException(404, "Fichier introuvable") raise HTTPException(404, "Fichier introuvable")
base, number = parse(current["title"]) cur = parse_title(current["title"])
prev_ep = next_ep = None prev_ep = next_ep = None
for row in rows: for row in rows:
other_base, other_num = parse(row["title"]) other = parse_title(row["title"])
if other_base != base or other_num is None or row["id"] == download_id: if (
other["series"] != cur["series"]
or other["season"] != cur["season"]
or other["episode"] is None
or row["id"] == download_id
):
continue continue
if number is not None and other_num == number - 1: if cur["episode"] is not None and other["episode"] == cur["episode"] - 1:
prev_ep = row["id"] prev_ep = row["id"]
if number is not None and other_num == number + 1: if cur["episode"] is not None and other["episode"] == cur["episode"] + 1:
next_ep = row["id"] next_ep = row["id"]
return {"previous": prev_ep, "next": next_ep} return {"previous": prev_ep, "next": next_ep}
+29
View File
@@ -314,6 +314,35 @@ button { font-family: inherit; }
} }
.episode-name { flex: 1; font-size: 0.92rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #d6d6dc; } .episode-name { flex: 1; font-size: 0.92rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #d6d6dc; }
/* ------------------------------------------------------------ bibliothèque */
.lib-groups { display: flex; flex-direction: column; gap: 0.7rem; }
.lib-group { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.lib-group-head {
display: flex;
align-items: center;
gap: 0.8rem;
width: 100%;
padding: 0.75rem 1.1rem;
background: none;
border: none;
color: inherit;
font-size: 1rem;
cursor: pointer;
text-align: left;
user-select: none;
transition: background 0.15s;
}
.lib-group-head:hover { background: var(--surface-2); }
.lib-chevron { color: var(--text-dim); font-size: 0.72rem; width: 0.9rem; flex-shrink: 0; }
.lib-group-name { flex: 1; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lib-group-meta { font-size: 0.78rem; color: var(--text-dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
.lib-group-body { padding: 0.7rem; border-top: 1px solid var(--border); }
.lib-group-body .episode-row { background: transparent; }
.lib-group-body .episode-row:hover { background: var(--surface-2); }
.section-title .badge { vertical-align: middle; margin-left: 0.5rem; }
/* ------------------------------------------------------------ téléchargements */ /* ------------------------------------------------------------ téléchargements */
.progress { .progress {
+81 -15
View File
@@ -4,27 +4,54 @@
{% block content %} {% block content %}
<h1 class="page-title">Bibliothèque</h1> <h1 class="page-title">Bibliothèque</h1>
<p class="page-sub">Tes fichiers téléchargés, prêts à être regardés.</p> <p class="page-sub">Tes fichiers téléchargés, regroupés par série et par dossier.</p>
<div x-data="libraryPage()" x-init="load()"> <div x-data="libraryPage()" x-init="load()">
<div class="spinner" x-show="loading"></div> <div class="spinner" x-show="loading"></div>
<template x-if="!loading && items.length === 0"> <template x-if="!loading && sections.length === 0">
<div class="empty-state"><div class="big">🎞️</div>Rien ici pour l'instant — télécharge des épisodes !</div> <div class="empty-state"><div class="big">🎞️</div>Rien ici pour l'instant — télécharge des épisodes !</div>
</template> </template>
<div class="episode-list"> <template x-for="section in sections" :key="section.key">
<template x-for="f in items" :key="f.id"> <section>
<a class="episode-row" :href="'/watch/' + f.id"> <h2 class="section-title">
<span style="font-size:1.3rem">🎬</span> <span x-text="section.label"></span>
<span class="episode-name" style="font-weight:600" x-text="f.title"></span> <span class="badge badge-type" x-text="section.count + ' fichier' + (section.count > 1 ? 's' : '')"></span>
<span style="font-size:0.8rem;color:var(--text-dim)" x-text="fmtBytes(f.total_bytes)"></span> </h2>
<span x-show="f.position_seconds > 0" class="badge badge-type"
x-text="'⏵ ' + fmtTime(f.position_seconds)"></span> <div class="lib-groups">
<span class="btn btn-sm">▶</span> <template x-for="g in section.groups" :key="section.key + '|' + g.series + '|' + g.folder">
</a> <div class="lib-group">
</template> <button type="button" class="lib-group-head" @click="g.open = !g.open">
</div> <span class="lib-chevron" x-text="g.open ? '▾' : '▸'"></span>
<span class="lib-group-name" x-text="g.series"></span>
<span class="badge badge-type" x-text="g.items.length + ' ép.'"></span>
<span x-show="g.folder" class="badge badge-version" :title="g.folder"
x-text="'📁 ' + g.folder"></span>
<span x-show="g.watched > 0" class="lib-group-meta"
x-text="'👁 ' + g.watched + '/' + g.items.length"></span>
<span class="lib-group-meta" x-text="fmtBytes(g.total)"></span>
</button>
<div class="episode-list lib-group-body" x-show="g.open" x-transition>
<template x-for="f in g.items" :key="f.id">
<a class="episode-row" :href="'/watch/' + f.id">
<span class="episode-num"
x-text="f.episode != null ? 'E' + f.episode : '🎬'"></span>
<span class="episode-name" style="font-weight:600" x-text="shortTitle(g, f)"></span>
<span style="font-size:0.8rem;color:var(--text-dim)" x-text="fmtBytes(f.total_bytes)"></span>
<span x-show="f.position_seconds > 0" class="badge badge-type"
x-text="'⏵ ' + fmtTime(f.position_seconds)"></span>
<span class="btn btn-sm">▶</span>
</a>
</template>
</div>
</div>
</template>
</div>
</section>
</template>
</div> </div>
{% endblock %} {% endblock %}
@@ -32,12 +59,51 @@
<script> <script>
function libraryPage() { function libraryPage() {
return { return {
items: [], loading: true, items: [], sections: [], loading: true,
async load() { async load() {
const res = await fetch('/api/library'); const res = await fetch('/api/library');
this.items = await res.json(); this.items = await res.json();
this.sections = [
{ key: 'anime', label: '⛩ Animés' },
{ key: 'serie', label: '📺 Séries & Films' },
]
.map(s => {
const groups = this.buildGroups(this.items.filter(i => i.media_type === s.key));
return { ...s, groups, count: groups.reduce((n, g) => n + g.items.length, 0) };
})
.filter(s => s.groups.length > 0);
this.loading = false; this.loading = false;
}, },
buildGroups(items) {
const map = new Map();
for (const f of items) {
const key = (f.series || f.title) + '|' + (f.folder || '');
if (!map.has(key)) map.set(key, { series: f.series || f.title, folder: f.folder || '', items: [], open: false });
map.get(key).items.push(f);
}
const groups = [...map.values()].sort((a, b) => a.series.localeCompare(b.series, 'fr'));
for (const g of groups) {
g.items.sort((a, b) =>
(a.season ?? 0) - (b.season ?? 0) ||
(a.episode ?? Infinity) - (b.episode ?? Infinity) ||
a.title.localeCompare(b.title, 'fr'));
g.total = g.items.reduce((n, i) => n + (i.total_bytes || 0), 0);
g.watched = g.items.filter(i => i.position_seconds > 0).length;
}
if (groups.length === 1) groups[0].open = true;
return groups;
},
shortTitle(g, f) {
let t = f.title;
if (t.toLowerCase().startsWith(g.series.toLowerCase())) {
t = t.slice(g.series.length).replace(/^[\s\-–—_.:]+/, '');
}
return t || f.title;
},
fmtBytes(n) { fmtBytes(n) {
if (n == null) return ''; if (n == null) return '';
const units = ['o', 'Ko', 'Mo', 'Go']; let i = 0; const units = ['o', 'Ko', 'Mo', 'Go']; let i = 0;
+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 # Installation guidée (recommandée) : ./scripts/install.sh
# Ou à la main : # Ou à la main :
# 1. docker login git.lanro.eu (compte Gitea avec accès lecture au repo) # 1. cp .env.example .env && $EDITOR .env (OHM_SECRET_KEY + WATCHTOWER_TOKEN obligatoires)
# 2. cp .env.example .env && $EDITOR .env (OHM_SECRET_KEY + WATCHTOWER_TOKEN obligatoires) # 2. docker compose up -d
# 3. 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 : # Mise à jour : page Admin → « Mise à jour », ou manuellement :
# docker compose pull && docker compose up -d # docker compose pull && docker compose up -d
services: services:
ohm: 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 local (dev) : décommenter ces lignes et commenter « image: » ci-dessus
# build: # build:
# context: . # context: .
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "ohm-stream" name = "ohm-stream"
version = "0.2.0" version = "0.3.0"
description = "Ohm Stream Downloader — centre de contrôle auto-hébergé pour animes et séries VOSTFR" description = "Ohm Stream Downloader — centre de contrôle auto-hébergé pour animes et séries VOSTFR"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
+2 -18
View File
@@ -5,7 +5,7 @@
# bash scripts/install.sh (depuis un clone du dépôt) # bash scripts/install.sh (depuis un clone du dépôt)
# curl -fsSL <gitea>/raw/branch/main/scripts/install.sh | bash # 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) # (les valeurs passées en option court-circuitent les questions)
# #
# Pose les questions (destination des animés, port), génère les secrets, # 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"; } warn() { printf " ${YELLOW}⚠${RESET} %s\n" "$1"; }
die() { printf " ${RED}✖ %s${RESET}\n" "$1" >&2; exit 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 while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--dir) ARG_DIR="$2"; shift 2;; --dir) ARG_DIR="$2"; shift 2;;
--port) ARG_PORT="$2"; shift 2;; --port) ARG_PORT="$2"; shift 2;;
--skip-login) ARG_SKIP_LOGIN=1; shift;;
*) shift;; *) shift;;
esac esac
done done
@@ -74,21 +73,6 @@ if [ ! -f "$COMPOSE_FILE" ]; then
fi fi
ok "Projet prêt dans $(pwd)" 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 # ---------------------------------------------------------------- destination des épisodes
step "Destination des épisodes téléchargés" step "Destination des épisodes téléchargés"
+9 -6
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Publie une version d'OhmStreaming : # 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 ["patchnote multi-lignes"] # 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. # Option : OHM_REGISTRY=git.lanro.eu/roman/ohm_streaming (défaut) pour viser un autre registre.
@@ -8,6 +8,7 @@ set -euo pipefail
VERSION="${1:?Usage : ./scripts/release.sh <version> [patchnote] (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}" REGISTRY="${OHM_REGISTRY:-git.lanro.eu/roman/ohm_streaming}"
GHCR="${OHM_GHCR:-ghcr.io/aulyriusohm/ohm-streaming}"
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "✖ « $VERSION » n'est pas un semver X.Y.Z"; exit 1; } [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "✖ « $VERSION » n'est pas un semver X.Y.Z"; exit 1; }
@@ -34,11 +35,13 @@ git tag -a "$TAG" -m "${2:-$TAG}"
git push origin HEAD git push origin HEAD
git push origin "$TAG" git push origin "$TAG"
echo "▶ 3/3 Image Docker → $REGISTRY" echo "▶ 3/3 Image Docker → $GHCR (public) + $REGISTRY (Gitea)"
docker build --build-arg VERSION="$VERSION" -t "$REGISTRY:$VERSION" -t "$REGISTRY:latest" . docker build --build-arg VERSION="$VERSION" -t "$GHCR:$VERSION" -t "$GHCR:latest" .
docker push "$REGISTRY:$VERSION" docker push "$GHCR:$VERSION" && docker push "$GHCR:latest"
docker push "$REGISTRY:latest" docker tag "$GHCR:$VERSION" "$REGISTRY:$VERSION"
docker tag "$GHCR:latest" "$REGISTRY:latest"
docker push "$REGISTRY:$VERSION" && docker push "$REGISTRY:latest"
echo 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 »." echo " Les instances la détecteront via Admin → Mise à jour → « Vérifier »."