Onglet Radio sur le serveur, bouton de synchronisation sur le mobile (2.5.0)
Le serveur gagne un onglet Radio : les stations de l'annuaire, écoutables en direct dans le navigateur ou enregistrables — jusqu'à l'arrêt ou pour une durée choisie —, puis réécoutables depuis l'interface. Quatre enregistrements en parallèle au plus ; si le flux tombe, il est rouvert et le fichier continue. Les enregistrements rejoignent la bibliothèque : le mobile les reçoit. Un redémarrage du service clôt proprement les captures en cours, et celles restées « en cours » après un plantage sont récupérées au démarrage suivant. La copie de flux sans blocs ICY, propre au guet, est extraite pour être partagée. Côté mobile, la synchronisation manuelle n'était qu'un petit lien dans l'onglet Connexion : elle a maintenant un vrai bouton dans l'onglet Synchronisation. Deux passages ne se chevauchent plus, et une erreur inattendue s'affiche au lieu de fermer l'application. Le code Kotlin n'a pas été compilé sur cette machine (pas de JDK) : il a été relu, mais reste à construire. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N6pmSE168yjyf8vgV8mZx6
This commit is contained in:
1 parent
7810bef8a3
commit
5b5e90247f
15 files changed
+593
-48
No files matched your search
@@ -28,6 +28,7 @@ from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, Res
|
||||
|
||||
from . import config as config_module
|
||||
from . import rotation
|
||||
from .recorder import AlreadyRecording, Recorder, TooMany
|
||||
from .sources import catalogs, radio
|
||||
from .storage import Storage
|
||||
from .workers import Downloader, Watcher, safe_name
|
||||
@@ -43,6 +44,7 @@ config = config_module.load()
|
||||
storage = Storage(config.db_path)
|
||||
downloader = Downloader(config, storage)
|
||||
watcher = Watcher(config, storage)
|
||||
recorder = Recorder(config, storage)
|
||||
|
||||
|
||||
def authorised(x_noxely_token: str = Header(default="")) -> None:
|
||||
@@ -62,11 +64,15 @@ def authorised(x_noxely_token: str = Header(default="")) -> None:
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
recorder.recover()
|
||||
tasks = [
|
||||
asyncio.create_task(downloader.run_forever()),
|
||||
asyncio.create_task(watcher.run_forever()),
|
||||
]
|
||||
yield
|
||||
# Les enregistrements en cours sont clos avant de couper : sans cela, redémarrer le
|
||||
# service laisserait chaque fichier « en cours » à jamais.
|
||||
await recorder.stop_all()
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
@@ -103,7 +109,8 @@ def media_file(media_id: str):
|
||||
|
||||
|
||||
@app.delete("/api/media/{media_id}", dependencies=[Depends(authorised)])
|
||||
def delete_media(media_id: str) -> dict:
|
||||
async def delete_media(media_id: str) -> dict:
|
||||
recorder.stop(media_id) # supprimer un enregistrement en cours l'arrête d'abord
|
||||
item = storage.delete_media(media_id)
|
||||
if item:
|
||||
(config.media_dir / item.filename).unlink(missing_ok=True)
|
||||
@@ -168,6 +175,44 @@ async def radio_stations(genre: str | None = None, name: str | None = None) -> d
|
||||
return {"stations": await radio.stations(genre, name)}
|
||||
|
||||
|
||||
@app.post("/api/radio/record", dependencies=[Depends(authorised)])
|
||||
async def radio_record(payload: dict = Body(...)) -> dict:
|
||||
"""Enregistre une station jusqu'à l'arrêt, ou jusqu'à `minutes` si elles sont données."""
|
||||
url = str(payload.get("url") or "").strip()
|
||||
if not url.startswith(("http://", "https://")):
|
||||
raise HTTPException(status_code=400, detail="Adresse de flux invalide")
|
||||
try:
|
||||
minutes = int(payload["minutes"]) if payload.get("minutes") else None
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(status_code=400, detail="Durée invalide")
|
||||
station = {
|
||||
"name": str(payload.get("name") or "Station").strip()[:80] or "Station",
|
||||
"url": url,
|
||||
"codec": str(payload.get("codec") or ""),
|
||||
}
|
||||
try:
|
||||
return {"media_id": recorder.start(station, minutes)}
|
||||
except AlreadyRecording:
|
||||
raise HTTPException(status_code=409, detail="Cette station est déjà en cours d'enregistrement")
|
||||
except TooMany as error:
|
||||
raise HTTPException(status_code=409, detail=f"{error} enregistrements simultanés au plus")
|
||||
|
||||
|
||||
@app.post("/api/radio/record/{media_id}/stop", dependencies=[Depends(authorised)])
|
||||
async def radio_stop(media_id: str) -> dict:
|
||||
return {"stopped": recorder.stop(media_id)}
|
||||
|
||||
|
||||
@app.get("/api/radio/recordings", dependencies=[Depends(authorised)])
|
||||
async def radio_recordings() -> dict:
|
||||
"""Ce qui s'enregistre en ce moment, et ce qui l'a été — guets compris."""
|
||||
return {
|
||||
"active": recorder.active(),
|
||||
"items": [m.public() for m in storage.radio_media()],
|
||||
"max_minutes": config.record_max_minutes,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/watches", dependencies=[Depends(authorised)])
|
||||
def watches() -> dict:
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user