From 05770e42180eadf79a5d2bcf72c82e8733ee1216 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Mon, 19 Oct 2026 12:09:28 +0200 Subject: [PATCH] =?UTF-8?q?Multi-IPTV=20:=20la=20lecture=20filtre=20comme?= =?UTF-8?q?=20l'=C3=A9criture=20rattache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les enregistrements, season passes et la règle Auto NBA s'affichaient encore sous les deux IPTV. Lecture et écriture ne résolvaient pas la playlist de la même façon : la création passait par get_playlist, qui se replie sur la playlist la plus récente, alors que les listes passaient par selected_playlist_id, qui exigeait un choix explicite du client et ne filtrait rien du tout sans lui. En-tête X-Playlist-Id absent ou non recevable, tout ressortait mélangé — et pour Auto NBA le repli rendait toutes les règles du compte. - selected_playlist_id devient scoped_playlist_id et résout via get_playlist, exactement comme à l'écriture ; None signifie désormais « compte sans playlist », plus « ne filtre pas ». - Listes des enregistrements et des season passes : égalité stricte sur playlist_id au lieu de is_none_or, qui gardait les lignes sans playlist sous chaque IPTV. Les rattacher est le travail de attach_legacy_rows_to_playlists, rejoué à chaque ouverture de la base, donc aucune ligne ne devient invisible. - Les trois chemins de création (enregistrement, season pass, règle) utilisent le même helper au lieu de recopier la résolution. Non compilé localement : ni cargo ni Docker sur cette machine. Co-Authored-By: Claude Opus 5 --- server/src/api/auto_rules.rs | 11 +++++------ server/src/api/recordings.rs | 29 +++++++++++++---------------- server/src/state.rs | 24 ++++++++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/server/src/api/auto_rules.rs b/server/src/api/auto_rules.rs index c201fc2..b056e31 100644 --- a/server/src/api/auto_rules.rs +++ b/server/src/api/auto_rules.rs @@ -30,8 +30,10 @@ pub async fn auto_rules(state: &SharedState, req: &Req, rest: &str) -> Resp { }; let parts = segments(rest); if let ("GET" | "HEAD", []) = (req.method.as_str(), parts.as_slice()) { - // Une règle NBA par IPTV : celle de la playlist choisie. - let rules = match state.selected_playlist_id(req) { + // Une règle NBA par IPTV : celle de la playlist courante, résolue + // comme à la création. Le repli sur toutes les règles du compte + // montrait la règle d'une IPTV sous les autres. + let rules = match state.scoped_playlist_id(req) { Some(pid) => state.db.get_auto_rules_for_playlist(&user.id, &pid), None => state.db.get_auto_rules_for_user(&user.id), }; @@ -148,10 +150,7 @@ async fn create(state: &SharedState, req: &Req, user: &User) -> Resp { Ok(b) => b, Err(r) => return r, }; - let playlist_id = state - .get_playlist(req) - .map(|p| p.id) - .filter(|id| !id.is_empty()); + let playlist_id = state.scoped_playlist_id(req); let existing = match &playlist_id { Some(pid) => state.db.get_auto_rules_for_playlist(&user.id, pid), None => state.db.get_auto_rules_for_user(&user.id), diff --git a/server/src/api/recordings.rs b/server/src/api/recordings.rs index 7ea83a9..ac6367f 100644 --- a/server/src/api/recordings.rs +++ b/server/src/api/recordings.rs @@ -62,9 +62,11 @@ async fn list(state: &SharedState, req: &Req) -> Resp { Some(user) if !user.is_admin => state.db.get_user_recordings(&user.id), _ => state.db.get_all_recordings(), }; - // Une IPTV choisie : seulement ses enregistrements. - if let Some(pid) = state.selected_playlist_id(req) { - recordings.retain(|r| r.playlist_id.as_deref().is_none_or(|p| p == pid)); + // Chaque IPTV n'affiche que ses propres enregistrements. Une ligne sans + // playlist n'appartient à aucune : la rattacher au démarrage est le + // travail de `attach_legacy_rows_to_playlists`, pas l'afficher partout. + if let Some(pid) = state.scoped_playlist_id(req) { + recordings.retain(|r| r.playlist_id.as_deref() == Some(pid.as_str())); } let now = Utc::now(); let mut enriched = Vec::with_capacity(recordings.len()); @@ -188,11 +190,9 @@ fn create(state: &SharedState, req: &Req) -> Resp { ); } let title = to_text(data.get("title")).unwrap_or_else(|| "Sans Titre".into()); - // Capture sur l'IPTV choisie par le client (à défaut, la plus récente). - let playlist_id = state - .get_playlist(req) - .map(|p| p.id) - .filter(|id| !id.is_empty()); + // Capture sur l'IPTV choisie par le client (à défaut, la plus récente) — + // la même que celle sous laquelle la liste s'affichera. + let playlist_id = state.scoped_playlist_id(req); let stream_url = match &playlist_id { Some(pid) => stream_url_for_playlist(&stream_url, pid), None => stream_url, @@ -285,11 +285,11 @@ pub fn season_passes(state: &SharedState, req: &Req, rest: &str) -> Resp { } else { state.db.get_season_passes_for_user(&user.id) }; - if let Some(pid) = state.selected_playlist_id(req) { + // Comme les enregistrements : un season pass appartient à une + // seule IPTV, et une ligne sans playlist à aucune. + if let Some(pid) = state.scoped_playlist_id(req) { passes.retain(|p| { - p.get("playlist_id") - .and_then(Value::as_str) - .is_none_or(|v| v == pid) + p.get("playlist_id").and_then(Value::as_str) == Some(pid.as_str()) }); } json( @@ -375,10 +375,7 @@ fn create_pass(state: &SharedState, req: &Req) -> Resp { &json!({"error": "Un Season Pass identique existe déjà"}), ); } - let playlist_id = state - .get_playlist(req) - .map(|p| p.id) - .filter(|id| !id.is_empty()); + let playlist_id = state.scoped_playlist_id(req); let stream_url = match &playlist_id { Some(pid) => stream_url_for_playlist(&stream_url, pid), None => stream_url, diff --git a/server/src/state.rs b/server/src/state.rs index 7b38e02..731307f 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -37,18 +37,22 @@ pub struct AppState { } impl AppState { - /// Playlist explicitement choisie par le client (en-tête ou paramètre), - /// si l'utilisateur y a droit. Sert à filtrer les listes par IPTV ; sans - /// choix explicite (anciens clients), rien n'est filtré. - pub fn selected_playlist_id(&self, req: &Req) -> Option { - let requested = req - .query_param("playlist") - .filter(|p| !p.trim().is_empty()) - .or_else(|| req.header("x-playlist-id").map(str::to_string))?; - let requested = requested.trim().to_string(); + /// IPTV de rattachement des listes (enregistrements, season passes, + /// règles automatiques). + /// + /// Résolue exactement comme à l'écriture, via [`Self::get_playlist`] : + /// le choix explicite du client s'il est recevable, sinon la playlist la + /// plus récente. Les deux côtés doivent répondre la même chose, sinon une + /// ligne écrite sous la playlist de repli reste visible sous toutes les + /// autres — c'est ce que faisait la lecture quand l'en-tête manquait, en + /// ne filtrant alors rien du tout. + /// + /// `None` signifie « ce compte n'a aucune playlist », pas « ne filtre + /// pas ». + pub fn scoped_playlist_id(&self, req: &Req) -> Option { self.get_playlist(req) .map(|p| p.id) - .filter(|id| *id == requested) + .filter(|id| !id.is_empty()) } /// Playlist Xtream de la requête.