Multi-IPTV : la lecture filtre comme l'écriture rattache

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 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-10-19 12:09:28 +02:00
1 parent 4f978755fd
commit 05770e4218
3 files changed
+32 -32

No files matched your search

+5 -6
View File
@@ -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),
+13 -16
View File
@@ -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,
+14 -10
View File
@@ -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<String> {
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<String> {
self.get_playlist(req)
.map(|p| p.id)
.filter(|id| *id == requested)
.filter(|id| !id.is_empty())
}
/// Playlist Xtream de la requête.