mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-20 21:48:06 +02:00
Multi-IPTV : chaque fonction utilise la playlist sélectionnée
Le serveur ignorait l'IPTV choisie : direct, VOD, guide, enregistrements, season passes et Auto NBA passaient tous par la playlist la plus récente (ou celle du premier utilisateur pour la capture FFmpeg), et les enregistrements s'affichaient sous toutes les playlists. - get_playlist respecte ?playlist=<id> (prioritaire) puis l'en-tête X-Playlist-Id, avec contrôle du propriétaire (admin : toutes). - Sessions FFmpeg HLS/VOD séparées par IPTV (même stream_id possible) ; le m3u8 propage ?playlist= aux segments, redirections et bascule .ts → HLS du lecteur web aussi. - tv_recordings, season_passes, auto_rules : colonne playlist_id (migrations v4-v6) ; lignes existantes rattachées à la playlist la plus récente de leur propriétaire ; URL de capture avec ?playlist= ; listes filtrées par IPTV ; une règle NBA par IPTV. - Proxy /api/xtream : cache d'allowlist par playlist, paramètre retiré avant l'appel au panneau ; CORS autorise X-Playlist-Id. - Client : AuthedHttp envoie X-Playlist-Id ; XtreamService ajoute ?playlist= aux flux, à l'EPG et aux appels xtream-api (cache distinct). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29b4e11d13
commit
4f978755fd
15 files changed
+501
-70
No files matched your search
+91
-4
@@ -67,6 +67,7 @@ fn recording_from_row(row: &Row) -> DbResult<Recording> {
|
||||
file_path: row.get("file_path")?,
|
||||
error_reason: row.get("error_reason")?,
|
||||
source_rule_id: row.get("source_rule_id")?,
|
||||
playlist_id: row.get("playlist_id")?,
|
||||
created_at: parse_date(row.get("created_at")?),
|
||||
updated_at: parse_date(row.get("updated_at")?),
|
||||
})
|
||||
@@ -98,6 +99,7 @@ fn auto_rule_from_row(row: &Row) -> DbResult<AutoRule> {
|
||||
pad_after_min: row.get("pad_after_min")?,
|
||||
team_filter: json_list(row.get("team_filter")?),
|
||||
keep_days: row.get("keep_days")?,
|
||||
playlist_id: row.get("playlist_id")?,
|
||||
created_at: row
|
||||
.get::<_, Option<String>>("created_at")?
|
||||
.unwrap_or_default(),
|
||||
@@ -191,6 +193,7 @@ impl AppDatabase {
|
||||
};
|
||||
db.create_tables()?;
|
||||
db.run_migrations()?;
|
||||
db.attach_legacy_rows_to_playlists()?;
|
||||
println!("Database initialized: {path}");
|
||||
Ok(db)
|
||||
}
|
||||
@@ -309,7 +312,6 @@ impl AppDatabase {
|
||||
CREATE INDEX IF NOT EXISTS idx_recordings_start ON tv_recordings(start_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_season_passes_user ON season_passes(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_auto_rules_user ON auto_rules(user_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_auto_rules_user_kind ON auto_rules(user_id, kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_nba_games_tipoff ON nba_games(rule_id, tipoff_utc);
|
||||
"#,
|
||||
)
|
||||
@@ -332,7 +334,7 @@ impl AppDatabase {
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
|
||||
let migrations: [(i64, &str, &str, &str); 3] = [
|
||||
let migrations: [(i64, &str, &str, &str); 6] = [
|
||||
// v1 : colonne error_reason absente des bases d'avant son introduction.
|
||||
(1, "tv_recordings", "error_reason", "TEXT"),
|
||||
// v2 : mode de correspondance des season passes. 'contains' pour les
|
||||
@@ -346,6 +348,10 @@ impl AppDatabase {
|
||||
),
|
||||
// v3 : lien vers la règle automatique (badge « Auto NBA », sidecars).
|
||||
(3, "tv_recordings", "source_rule_id", "TEXT"),
|
||||
// v4-v6 : IPTV de rattachement (plusieurs playlists par compte).
|
||||
(4, "tv_recordings", "playlist_id", "TEXT"),
|
||||
(5, "season_passes", "playlist_id", "TEXT"),
|
||||
(6, "auto_rules", "playlist_id", "TEXT"),
|
||||
];
|
||||
for (version, table, column, definition) in migrations {
|
||||
if version <= current {
|
||||
@@ -375,6 +381,29 @@ impl AppDatabase {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lignes antérieures au multi-IPTV : rattachées à la playlist la plus
|
||||
/// récente de leur propriétaire, celle qu'elles utilisaient de fait.
|
||||
/// Sans cela elles s'afficheraient sous toutes les playlists. Rejoué à
|
||||
/// chaque démarrage, sans effet une fois tout rattaché.
|
||||
fn attach_legacy_rows_to_playlists(&self) -> DbResult<()> {
|
||||
let conn = self.lock();
|
||||
for table in ["tv_recordings", "season_passes", "auto_rules"] {
|
||||
conn.execute(
|
||||
&format!(
|
||||
"UPDATE {table} SET playlist_id = (SELECT p.id FROM playlists p WHERE p.user_id = {table}.user_id ORDER BY p.created_at DESC LIMIT 1) WHERE playlist_id IS NULL"
|
||||
),
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
// Une règle NBA par utilisateur ET par IPTV.
|
||||
conn.execute_batch(
|
||||
"DROP INDEX IF EXISTS idx_auto_rules_user_kind;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_auto_rules_user_kind_playlist
|
||||
ON auto_rules(user_id, kind, IFNULL(playlist_id, ''));",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Crée le compte admin s'il n'existe aucun utilisateur.
|
||||
///
|
||||
/// Le mot de passe initial vient de ADMIN_INITIAL_PASSWORD, ou est généré
|
||||
@@ -731,6 +760,7 @@ impl AppDatabase {
|
||||
file_path: None,
|
||||
error_reason: None,
|
||||
source_rule_id: None,
|
||||
playlist_id: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
@@ -909,6 +939,22 @@ impl AppDatabase {
|
||||
.next()
|
||||
}
|
||||
/// Marque un enregistrement comme créé par une règle automatique.
|
||||
/// Rattache un enregistrement à l'IPTV qui le capture.
|
||||
pub fn set_recording_playlist(&self, id: &str, playlist_id: &str) {
|
||||
let _ = self.lock().execute(
|
||||
"UPDATE tv_recordings SET playlist_id = ? WHERE id = ?",
|
||||
params![playlist_id, id],
|
||||
);
|
||||
}
|
||||
|
||||
/// Rattache un season pass à son IPTV.
|
||||
pub fn set_season_pass_playlist(&self, id: &str, playlist_id: &str) {
|
||||
let _ = self.lock().execute(
|
||||
"UPDATE season_passes SET playlist_id = ? WHERE id = ?",
|
||||
params![playlist_id, id],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_recording_source(&self, id: &str, rule_id: &str) {
|
||||
let _ = self.lock().execute(
|
||||
"UPDATE tv_recordings SET source_rule_id = ? WHERE id = ?",
|
||||
@@ -925,7 +971,7 @@ impl AppDatabase {
|
||||
rule.created_at = now.clone();
|
||||
rule.updated_at = now;
|
||||
self.lock().execute(
|
||||
"INSERT INTO auto_rules (id, user_id, kind, enabled, default_decision, channel_ids, pad_before_min, pad_after_min, team_filter, keep_days, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO auto_rules (id, user_id, kind, enabled, default_decision, channel_ids, pad_before_min, pad_after_min, team_filter, keep_days, created_at, updated_at, playlist_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
params![
|
||||
rule.id,
|
||||
rule.user_id,
|
||||
@@ -938,7 +984,8 @@ impl AppDatabase {
|
||||
serde_json::to_string(&rule.team_filter).unwrap_or_else(|_| "[]".into()),
|
||||
rule.keep_days,
|
||||
rule.created_at,
|
||||
rule.updated_at
|
||||
rule.updated_at,
|
||||
rule.playlist_id
|
||||
],
|
||||
)?;
|
||||
Ok(rule)
|
||||
@@ -988,6 +1035,14 @@ impl AppDatabase {
|
||||
)
|
||||
}
|
||||
|
||||
/// Règles d'un utilisateur pour une IPTV donnée.
|
||||
pub fn get_auto_rules_for_playlist(&self, user_id: &str, playlist_id: &str) -> Vec<AutoRule> {
|
||||
self.query_auto_rules(
|
||||
"SELECT * FROM auto_rules WHERE user_id = ? AND playlist_id = ? ORDER BY created_at ASC",
|
||||
&[user_id, playlist_id],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_auto_rule(&self, id: &str) -> Option<AutoRule> {
|
||||
self.query_auto_rules("SELECT * FROM auto_rules WHERE id = ?", &[id])
|
||||
.into_iter()
|
||||
@@ -1223,6 +1278,7 @@ mod tests {
|
||||
pad_after_min: 45,
|
||||
team_filter: vec![],
|
||||
keep_days: None,
|
||||
playlist_id: None,
|
||||
created_at: String::new(),
|
||||
updated_at: String::new(),
|
||||
})
|
||||
@@ -1326,6 +1382,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn une_regle_nba_par_iptv_et_rattachement_des_anciennes_lignes() {
|
||||
let (db, path) = temp_db();
|
||||
let user = db.create_user("u", "x", false).unwrap();
|
||||
// Ligne « ancienne » : règle sans playlist, créée avant les playlists.
|
||||
let legacy = nba_rule(&db, &user.id);
|
||||
let a = db
|
||||
.create_playlist(&user.id, "A", "http://a", "u", "p", None)
|
||||
.unwrap();
|
||||
// Au redémarrage, l'ancienne règle est rattachée à la playlist A.
|
||||
drop(db);
|
||||
let db = AppDatabase::open(path.to_str().unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
db.get_auto_rule(&legacy.id).unwrap().playlist_id.as_deref(),
|
||||
Some(a.id.as_str())
|
||||
);
|
||||
let b = db
|
||||
.create_playlist(&user.id, "B", "http://b", "u", "p", None)
|
||||
.unwrap();
|
||||
// Une seconde règle NBA est permise sur l'autre IPTV…
|
||||
let mut on_b = legacy.clone();
|
||||
on_b.playlist_id = Some(b.id.clone());
|
||||
db.create_auto_rule(&on_b).unwrap();
|
||||
assert_eq!(db.get_auto_rules_for_playlist(&user.id, &b.id).len(), 1);
|
||||
assert_eq!(db.get_auto_rules_for_playlist(&user.id, &a.id).len(), 1);
|
||||
// …mais pas deux sur la même.
|
||||
assert!(is_unique_violation(
|
||||
&db.create_auto_rule(&on_b).unwrap_err()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mot_de_passe_genere() {
|
||||
let p = generate_password(16);
|
||||
|
||||
Reference in new issue
Block a user