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.