mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-21 22:18:04 +02:00
Auto NBA : afficher les matchs beIN même sans chaîne reliée, diagnostic
L'onglet restait vide alors que le guide beIN annonçait des matchs : une diffusion n'était prise en compte que si sa chaîne beIN était reliée à un flux de la règle (liste vide si la détection avait échoué à la création, ou nom différent chez le revendeur). - Toute diffusion en direct du guide beIN est rapprochée et affichée ; seul l'enregistrement exige un flux IPTV (sinon « absente de la playlist », bouton inactif, aucun enregistrement créé). - Les beIN FR de la playlist sont redétectées à chaque passage et complètent les flux choisis dans la règle. - Plus besoin de playlist pour afficher le programme. - Diagnostic par règle (source, directs au guide, chaînes reliées ou absentes, matchs rapprochés) renvoyé par l'API et affiché quand la liste est vide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f0f6abcc6
commit
29b4e11d13
4 files changed
+235
-60
No files matched your search
@@ -1764,6 +1764,8 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
|
|||||||
bool _busy = false;
|
bool _busy = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
bool _showOffBein = false;
|
bool _showOffBein = false;
|
||||||
|
// Dernier diagnostic serveur : source, diffusions, chaînes reliées.
|
||||||
|
Map<String, dynamic>? _diag;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -1799,7 +1801,9 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_rule = rule;
|
_rule = rule;
|
||||||
_games = json.decode(g.body)['games'] as List;
|
final body = json.decode(g.body);
|
||||||
|
_games = body['games'] as List;
|
||||||
|
_diag = body['diagnostic'] as Map<String, dynamic>?;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1866,7 +1870,11 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _busy = false);
|
setState(() => _busy = false);
|
||||||
if (r.statusCode == 200) {
|
if (r.statusCode == 200) {
|
||||||
setState(() => _games = json.decode(r.body)['games'] as List);
|
final body = json.decode(r.body);
|
||||||
|
setState(() {
|
||||||
|
_games = body['games'] as List;
|
||||||
|
_diag = body['diagnostic'] as Map<String, dynamic>?;
|
||||||
|
});
|
||||||
notifyRecordingsChanged();
|
notifyRecordingsChanged();
|
||||||
} else {
|
} else {
|
||||||
_snack('❌ ${r.body}');
|
_snack('❌ ${r.body}');
|
||||||
@@ -2109,6 +2117,33 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Résumé du dernier passage serveur, pour comprendre une liste vide.
|
||||||
|
String? _diagText() {
|
||||||
|
final d = _diag;
|
||||||
|
if (d == null) return null;
|
||||||
|
final parts = <String>[];
|
||||||
|
final calendar = d['games_in_calendar'];
|
||||||
|
if (calendar == 0) parts.add('Calendrier NBA vide ou injoignable');
|
||||||
|
if (d['source'] == 'bein') {
|
||||||
|
parts.add('${d['live_broadcasts'] ?? 0} direct(s) NBA au guide beIN');
|
||||||
|
} else if (d['source'] == 'iptv') {
|
||||||
|
parts.add('Guide beIN injoignable, repli sur le guide IPTV');
|
||||||
|
}
|
||||||
|
if (d['has_playlist'] == false) parts.add('aucune playlist IPTV');
|
||||||
|
final linked = (d['linked_channels'] as List?) ?? const [];
|
||||||
|
parts.add(
|
||||||
|
linked.isEmpty
|
||||||
|
? 'aucune beIN trouvée dans la playlist'
|
||||||
|
: '${linked.length} beIN reliée(s) à la playlist',
|
||||||
|
);
|
||||||
|
final missing = (d['missing_channels'] as List?) ?? const [];
|
||||||
|
if (missing.isNotEmpty) parts.add('absentes : ${missing.join(', ')}');
|
||||||
|
if (d['matched_games'] != null) {
|
||||||
|
parts.add('${d['matched_games']} match(s) rapproché(s)');
|
||||||
|
}
|
||||||
|
return parts.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
/// Nuit de diffusion : un match à 02:30 appartient à la nuit de la veille.
|
/// Nuit de diffusion : un match à 02:30 appartient à la nuit de la veille.
|
||||||
String _nightLabel(DateTime local) {
|
String _nightLabel(DateTime local) {
|
||||||
final d = local.subtract(const Duration(hours: 12));
|
final d = local.subtract(const Duration(hours: 12));
|
||||||
@@ -2186,11 +2221,12 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
|
|||||||
final tipoff = DateTime.parse(g['tipoff_utc']).toLocal();
|
final tipoff = DateTime.parse(g['tipoff_utc']).toLocal();
|
||||||
final (label, color) = _status(g);
|
final (label, color) = _status(g);
|
||||||
final channel = g['channel_name'] as String?;
|
final channel = g['channel_name'] as String?;
|
||||||
// Pas annoncé en direct sur beIN : rien à enregistrer.
|
// Chaîne beIN reliée à un flux de la playlist : sinon pas d'enregistrement.
|
||||||
|
final recordable = g['channel_id'] != null;
|
||||||
final locked =
|
final locked =
|
||||||
['recorded', 'missed', 'not_on_bein'].contains(g['status']) ||
|
['recorded', 'missed', 'not_on_bein'].contains(g['status']) ||
|
||||||
tipoff.isBefore(DateTime.now()) ||
|
tipoff.isBefore(DateTime.now()) ||
|
||||||
(channel == null && g['wanted'] != true);
|
(!recordable && g['wanted'] != true);
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -2229,7 +2265,8 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${_hhmm(tipoff)} · ${channel ?? 'pas annoncé sur beIN'}',
|
'${_hhmm(tipoff)} · ${channel ?? 'pas annoncé sur beIN'}'
|
||||||
|
'${channel != null && !recordable ? ' · absente de la playlist' : ''}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.onSurface54,
|
color: AppColors.onSurface54,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -2343,7 +2380,8 @@ class _AutoNbaViewState extends State<_AutoNbaView> {
|
|||||||
return _emptyState(
|
return _emptyState(
|
||||||
Icons.event_busy,
|
Icons.event_busy,
|
||||||
'Aucun match NBA annoncé sur beIN Sports',
|
'Aucun match NBA annoncé sur beIN Sports',
|
||||||
'Le programme beIN couvre environ 8 jours et est relu toutes les 30 min',
|
_diagText() ??
|
||||||
|
'Le programme beIN couvre environ 8 jours et est relu toutes les 30 min',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Liste aplatie : intertitre de nuit puis ses matchs.
|
// Liste aplatie : intertitre de nuit puis ses matchs.
|
||||||
|
|||||||
@@ -267,7 +267,10 @@ fn games(state: &SharedState, req: &Req, rule: &AutoRule) -> Resp {
|
|||||||
game_json(rule, g, taken)
|
game_json(rule, g, taken)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
json(200, &json!({"rule": rule, "games": list}))
|
json(
|
||||||
|
200,
|
||||||
|
&json!({"rule": rule, "games": list, "diagnostic": state.nba.diagnostic(&rule.id)}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn decision(state: &SharedState, req: &Req, rule: &AutoRule, game_id: &str) -> Resp {
|
async fn decision(state: &SharedState, req: &Req, rule: &AutoRule, game_id: &str) -> Resp {
|
||||||
|
|||||||
@@ -264,17 +264,14 @@ impl EpgApi {
|
|||||||
self.programmes_with_source(playlist, channel_id).await.0
|
self.programmes_with_source(playlist, channel_id).await.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nom de la chaîne dans la table du panneau (cache 6 h).
|
/// Toutes les chaînes du panneau : `(stream_id, nom)` (cache 6 h).
|
||||||
pub async fn channel_name(
|
pub async fn channel_names(&self, playlist: &PlaylistConfig) -> Vec<(String, String)> {
|
||||||
&self,
|
|
||||||
playlist: &PlaylistConfig,
|
|
||||||
channel_id: &str,
|
|
||||||
) -> Option<String> {
|
|
||||||
self.channel_map_for(playlist)
|
self.channel_map_for(playlist)
|
||||||
.await
|
.await
|
||||||
.names
|
.names
|
||||||
.get(channel_id)
|
.iter()
|
||||||
.cloned()
|
.map(|(id, name)| (id.clone(), name.clone()))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn programmes_with_source(
|
async fn programmes_with_source(
|
||||||
|
|||||||
@@ -269,6 +269,13 @@ pub fn plan_action(
|
|||||||
None => Action::Status(past_or(game, tipoff, now, "provisional")),
|
None => Action::Status(past_or(game, tipoff, now, "provisional")),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
if live.programme.channel_id.is_empty() {
|
||||||
|
// Diffusé sur une beIN absente de la playlist : rien à capturer.
|
||||||
|
return match scheduled {
|
||||||
|
Some(_) => Action::Status("scheduled"),
|
||||||
|
None => Action::Status("confirmed"),
|
||||||
|
};
|
||||||
|
}
|
||||||
let (start, end) = recording_window(rule, tipoff, &live.programme);
|
let (start, end) = recording_window(rule, tipoff, &live.programme);
|
||||||
if end <= now {
|
if end <= now {
|
||||||
return Action::Status("missed");
|
return Action::Status("missed");
|
||||||
@@ -431,7 +438,18 @@ pub fn map_bein_broadcasts(
|
|||||||
previously_shown: !b.live,
|
previously_shown: !b.live,
|
||||||
}),
|
}),
|
||||||
None => {
|
None => {
|
||||||
if !unmapped.contains(&b.channel) {
|
// Affiché quand même : seul l'enregistrement exige un flux.
|
||||||
|
mapped.push(EpgProgramme {
|
||||||
|
channel_id: String::new(),
|
||||||
|
channel_label: b.channel.clone(),
|
||||||
|
title: b.title.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
start: b.start,
|
||||||
|
end: b.end,
|
||||||
|
is_live: b.live,
|
||||||
|
previously_shown: !b.live,
|
||||||
|
});
|
||||||
|
if b.live && !unmapped.contains(&b.channel) {
|
||||||
unmapped.push(b.channel.clone());
|
unmapped.push(b.channel.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,6 +458,45 @@ pub fn map_bein_broadcasts(
|
|||||||
(mapped, unmapped)
|
(mapped, unmapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flux IPTV à surveiller, avec leur libellé beIN : ceux choisis dans la
|
||||||
|
/// règle, complétés par les beIN FR détectées dans la playlist pour les
|
||||||
|
/// chaînes qu'aucun flux choisi ne couvre.
|
||||||
|
///
|
||||||
|
/// POURQUOI : une liste vide (détection ratée à la création) ou un flux
|
||||||
|
/// renommé par le revendeur faisait ignorer tout le guide beIN.
|
||||||
|
pub fn channel_labels(rule: &AutoRule, names: &[(String, String)]) -> Vec<(String, String)> {
|
||||||
|
let table: Vec<(String, String, String)> = names
|
||||||
|
.iter()
|
||||||
|
.map(|(id, name)| (id.clone(), name.clone(), String::new()))
|
||||||
|
.collect();
|
||||||
|
let detected = crate::services::bein_channels::detect(&table);
|
||||||
|
let mut labels: Vec<(String, String)> = Vec::new();
|
||||||
|
for id in &rule.channel_ids {
|
||||||
|
let label = detected
|
||||||
|
.iter()
|
||||||
|
.find(|c| &c.stream_id == id)
|
||||||
|
.map(|c| c.label.clone())
|
||||||
|
.or_else(|| {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.find(|(sid, _)| sid == id)
|
||||||
|
.map(|(_, n)| n.clone())
|
||||||
|
});
|
||||||
|
if let Some(label) = label {
|
||||||
|
labels.push((id.clone(), label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for c in crate::services::bein_channels::preferred_fr(&detected) {
|
||||||
|
let covered = labels
|
||||||
|
.iter()
|
||||||
|
.any(|(_, l)| normalize(l) == normalize(&c.label));
|
||||||
|
if !covered {
|
||||||
|
labels.push((c.stream_id, c.label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels
|
||||||
|
}
|
||||||
|
|
||||||
/// Service : calendrier quotidien + réconciliation toutes les 30 minutes.
|
/// Service : calendrier quotidien + réconciliation toutes les 30 minutes.
|
||||||
pub struct NbaAutoRecorder {
|
pub struct NbaAutoRecorder {
|
||||||
db: Arc<AppDatabase>,
|
db: Arc<AppDatabase>,
|
||||||
@@ -448,6 +505,8 @@ pub struct NbaAutoRecorder {
|
|||||||
timers: Mutex<Vec<JoinHandle<()>>>,
|
timers: Mutex<Vec<JoinHandle<()>>>,
|
||||||
/// Une réconciliation à la fois (timer et API peuvent se croiser).
|
/// Une réconciliation à la fois (timer et API peuvent se croiser).
|
||||||
running: tokio::sync::Mutex<()>,
|
running: tokio::sync::Mutex<()>,
|
||||||
|
/// Dernier diagnostic par règle (affiché dans l'onglet).
|
||||||
|
diagnostics: Mutex<HashMap<String, Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NbaAutoRecorder {
|
impl NbaAutoRecorder {
|
||||||
@@ -458,6 +517,7 @@ impl NbaAutoRecorder {
|
|||||||
epg,
|
epg,
|
||||||
timers: Mutex::new(Vec::new()),
|
timers: Mutex::new(Vec::new()),
|
||||||
running: tokio::sync::Mutex::new(()),
|
running: tokio::sync::Mutex::new(()),
|
||||||
|
diagnostics: Mutex::new(HashMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,51 +616,63 @@ impl NbaAutoRecorder {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Programmes EPG de toutes les chaînes surveillées par la règle.
|
/// Dernier diagnostic de la règle : source, diffusions trouvées,
|
||||||
async fn watched_programmes(
|
/// chaînes reliées ou absentes de la playlist.
|
||||||
|
pub fn diagnostic(&self, rule_id: &str) -> Option<Value> {
|
||||||
|
self.diagnostics.lock().unwrap().get(rule_id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diffusions à rapprocher des matchs : guide beIN d'abord (toutes les
|
||||||
|
/// beIN FR, reliées ou non à un flux), EPG IPTV en repli.
|
||||||
|
async fn collect_programmes(
|
||||||
&self,
|
&self,
|
||||||
rule: &AutoRule,
|
rule: &AutoRule,
|
||||||
playlist: &PlaylistConfig,
|
playlist: Option<&PlaylistConfig>,
|
||||||
) -> Vec<EpgProgramme> {
|
) -> (Vec<EpgProgramme>, Value) {
|
||||||
// Libellé beIN de chaque flux surveillé (« beIN SPORTS 1 »).
|
let names = match playlist {
|
||||||
let mut labels: Vec<(String, String)> = Vec::new();
|
Some(p) => self.epg.channel_names(p).await,
|
||||||
for channel_id in &rule.channel_ids {
|
None => Vec::new(),
|
||||||
let label = self
|
};
|
||||||
.epg
|
let labels = channel_labels(rule, &names);
|
||||||
.channel_name(playlist, channel_id)
|
let linked: Vec<&str> = labels.iter().map(|(_, l)| l.as_str()).collect();
|
||||||
.await
|
|
||||||
.map(|n| {
|
|
||||||
crate::services::bein_channels::classify(channel_id, &n, "")
|
|
||||||
.map(|c| c.label)
|
|
||||||
.unwrap_or(n)
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| format!("Chaîne {channel_id}"));
|
|
||||||
labels.push((channel_id.clone(), label));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source principale : le guide officiel beIN (≈ 8 jours, direct ou
|
|
||||||
// rediffusion explicites, chaîne exacte).
|
|
||||||
if let Some(broadcasts) = bein_guide::fetch_nba(&self.http, BEIN_GUIDE_DAYS).await {
|
if let Some(broadcasts) = bein_guide::fetch_nba(&self.http, BEIN_GUIDE_DAYS).await {
|
||||||
let (mapped, unmapped) = map_bein_broadcasts(&broadcasts, &labels);
|
let (progs, missing) = map_bein_broadcasts(&broadcasts, &labels);
|
||||||
if !unmapped.is_empty() {
|
if !missing.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"[NBA] Chaînes beIN absentes de la règle : {}",
|
"[NBA] beIN absentes de la playlist : {}",
|
||||||
unmapped.join(", ")
|
missing.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return mapped;
|
let diag = serde_json::json!({
|
||||||
|
"source": "bein",
|
||||||
|
"broadcasts": broadcasts.len(),
|
||||||
|
"live_broadcasts": broadcasts.iter().filter(|b| b.live).count(),
|
||||||
|
"linked_channels": linked,
|
||||||
|
"missing_channels": missing,
|
||||||
|
"playlist_channels": names.len(),
|
||||||
|
});
|
||||||
|
return (progs, diag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repli : l'EPG du panneau IPTV (≈ 48 h).
|
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for (channel_id, label) in &labels {
|
if let Some(p) = playlist {
|
||||||
for v in self.epg.programmes(playlist, channel_id).await {
|
for (channel_id, label) in &labels {
|
||||||
if let Some(p) = EpgProgramme::from_json(channel_id, label, &v) {
|
for v in self.epg.programmes(p, channel_id).await {
|
||||||
out.push(p);
|
if let Some(prog) = EpgProgramme::from_json(channel_id, label, &v) {
|
||||||
|
out.push(prog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
let diag = serde_json::json!({
|
||||||
|
"source": "iptv",
|
||||||
|
"broadcasts": out.len(),
|
||||||
|
"linked_channels": linked,
|
||||||
|
"missing_channels": [],
|
||||||
|
"playlist_channels": names.len(),
|
||||||
|
});
|
||||||
|
(out, diag)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Réconcilie calendrier, EPG et enregistrements d'une règle.
|
/// Réconcilie calendrier, EPG et enregistrements d'une règle.
|
||||||
@@ -622,23 +694,24 @@ impl NbaAutoRecorder {
|
|||||||
now - Duration::hours(6),
|
now - Duration::hours(6),
|
||||||
now + Duration::days(SCHEDULE_DAYS),
|
now + Duration::days(SCHEDULE_DAYS),
|
||||||
);
|
);
|
||||||
|
let playlist = self.playlist_for(&rule.user_id);
|
||||||
if games.is_empty() {
|
if games.is_empty() {
|
||||||
|
self.diagnostics.lock().unwrap().insert(
|
||||||
|
rule.id.clone(),
|
||||||
|
serde_json::json!({"games_in_calendar": 0, "at": utc_iso(now)}),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(playlist) = self.playlist_for(&rule.user_id) else {
|
|
||||||
println!("[NBA] Règle {} : aucune playlist", rule.id);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
// L'EPG ne couvre que quelques jours : inutile de le charger pour
|
// L'EPG ne couvre que quelques jours : inutile de le charger pour
|
||||||
// des matchs lointains seulement.
|
// des matchs lointains seulement.
|
||||||
let needs_epg = games.iter().any(|g| {
|
let needs_epg = games.iter().any(|g| {
|
||||||
DartDateTime::parse(&g.tipoff_utc)
|
DartDateTime::parse(&g.tipoff_utc)
|
||||||
.is_some_and(|t| t.to_utc() < now + Duration::days(BEIN_GUIDE_DAYS))
|
.is_some_and(|t| t.to_utc() < now + Duration::days(BEIN_GUIDE_DAYS))
|
||||||
});
|
});
|
||||||
let programmes = if needs_epg && !rule.channel_ids.is_empty() {
|
let (programmes, mut diag) = if needs_epg {
|
||||||
self.watched_programmes(rule, &playlist).await
|
self.collect_programmes(rule, playlist.as_ref()).await
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
(Vec::new(), serde_json::json!({"source": "none"}))
|
||||||
};
|
};
|
||||||
let tipoffs: HashMap<String, DateTime<Utc>> = games
|
let tipoffs: HashMap<String, DateTime<Utc>> = games
|
||||||
.iter()
|
.iter()
|
||||||
@@ -648,6 +721,8 @@ impl NbaAutoRecorder {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut created = 0;
|
let mut created = 0;
|
||||||
|
let mut matched = 0;
|
||||||
|
let in_calendar = games.len();
|
||||||
for game in games {
|
for game in games {
|
||||||
let Some(&tipoff) = tipoffs.get(&game.game_id) else {
|
let Some(&tipoff) = tipoffs.get(&game.game_id) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -664,7 +739,9 @@ impl NbaAutoRecorder {
|
|||||||
let action = plan_action(rule, &game, tipoff, live.as_ref(), recording.as_ref(), now);
|
let action = plan_action(rule, &game, tipoff, live.as_ref(), recording.as_ref(), now);
|
||||||
let mut next = game.clone();
|
let mut next = game.clone();
|
||||||
if let Some(l) = &live {
|
if let Some(l) = &live {
|
||||||
next.channel_id = Some(l.programme.channel_id.clone());
|
matched += 1;
|
||||||
|
next.channel_id =
|
||||||
|
(!l.programme.channel_id.is_empty()).then(|| l.programme.channel_id.clone());
|
||||||
next.channel_name = Some(l.programme.channel_label.clone());
|
next.channel_name = Some(l.programme.channel_label.clone());
|
||||||
next.epg_title = Some(l.programme.title.clone());
|
next.epg_title = Some(l.programme.title.clone());
|
||||||
next.epg_start = Some(utc_iso(l.programme.start));
|
next.epg_start = Some(utc_iso(l.programme.start));
|
||||||
@@ -750,6 +827,16 @@ impl NbaAutoRecorder {
|
|||||||
if created > 0 {
|
if created > 0 {
|
||||||
println!("[NBA] Règle {} : {created} match(s) programmé(s)", rule.id);
|
println!("[NBA] Règle {} : {created} match(s) programmé(s)", rule.id);
|
||||||
}
|
}
|
||||||
|
if let Value::Object(m) = &mut diag {
|
||||||
|
m.insert("games_in_calendar".into(), in_calendar.into());
|
||||||
|
m.insert("matched_games".into(), matched.into());
|
||||||
|
m.insert("has_playlist".into(), playlist.is_some().into());
|
||||||
|
m.insert("at".into(), utc_iso(now).into());
|
||||||
|
}
|
||||||
|
self.diagnostics
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(rule.id.clone(), diag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1084,13 +1171,63 @@ mod tests {
|
|||||||
];
|
];
|
||||||
let labels = vec![("101".to_string(), "beIN SPORTS 1".to_string())];
|
let labels = vec![("101".to_string(), "beIN SPORTS 1".to_string())];
|
||||||
let (progs, unmapped) = map_bein_broadcasts(&guide, &labels);
|
let (progs, unmapped) = map_bein_broadcasts(&guide, &labels);
|
||||||
assert_eq!(progs.len(), 2);
|
// Tout est gardé pour l'affichage ; le flux manque pour MAX 9.
|
||||||
assert!(progs.iter().all(|p| p.channel_id == "101"));
|
assert_eq!(progs.len(), 3);
|
||||||
|
assert!(progs[..2].iter().all(|p| p.channel_id == "101"));
|
||||||
|
assert_eq!(progs[2].channel_id, "");
|
||||||
|
assert_eq!(progs[2].channel_label, "beIN SPORTS MAX 9");
|
||||||
assert_eq!(unmapped, vec!["beIN SPORTS MAX 9".to_string()]);
|
assert_eq!(unmapped, vec!["beIN SPORTS MAX 9".to_string()]);
|
||||||
// Le direct est retenu ; la rediffusion du matin ne l'est pas.
|
// Le direct est retenu ; la rediffusion du matin ne l'est pas.
|
||||||
let live = find_live_broadcast("LAL", "BOS", t(TIP), false, &progs).unwrap();
|
let live = find_live_broadcast("LAL", "BOS", t(TIP), false, &progs[..2]).unwrap();
|
||||||
assert_eq!(live.programme.start, t("2026-10-22T02:30:00Z"));
|
assert_eq!(live.programme.start, t("2026-10-22T02:30:00Z"));
|
||||||
assert!(find_live_broadcast("LAL", "BOS", t(TIP), false, &progs[1..]).is_none());
|
assert!(find_live_broadcast("LAL", "BOS", t(TIP), false, &progs[1..2]).is_none());
|
||||||
|
// Diffusion sur une beIN absente de la playlist : affichée, pas programmée.
|
||||||
|
let miami = find_live_broadcast("MIA", "UTAH", t(TIP), false, &progs).unwrap();
|
||||||
|
assert_eq!(miami.programme.channel_label, "beIN SPORTS MAX 9");
|
||||||
|
let mut g = game("accepted");
|
||||||
|
g.home_tri = "MIA".into();
|
||||||
|
g.away_tri = "UTAH".into();
|
||||||
|
assert_eq!(
|
||||||
|
plan_action(
|
||||||
|
&rule(),
|
||||||
|
&g,
|
||||||
|
t(TIP),
|
||||||
|
Some(&miami),
|
||||||
|
None,
|
||||||
|
t("2026-10-21T10:00:00Z")
|
||||||
|
),
|
||||||
|
Action::Status("confirmed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chaines_bein_detectees_dans_la_playlist() {
|
||||||
|
let names: Vec<(String, String)> = [
|
||||||
|
("10", "FR - BEIN SPORTS 1 HD"),
|
||||||
|
("11", "FR - BEIN SPORTS 1 FHD"),
|
||||||
|
("20", "FR| beIN SPORTS 2"),
|
||||||
|
("30", "AR - BEIN SPORTS 3"),
|
||||||
|
("40", "FR - TF1"),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|(a, b)| (a.to_string(), b.to_string()))
|
||||||
|
.collect();
|
||||||
|
// Règle sans chaîne (détection ratée à la création) : tout est relié.
|
||||||
|
let mut r = rule();
|
||||||
|
r.channel_ids = vec![];
|
||||||
|
let labels = channel_labels(&r, &names);
|
||||||
|
assert_eq!(
|
||||||
|
labels,
|
||||||
|
vec![
|
||||||
|
("11".to_string(), "beIN SPORTS 1".to_string()),
|
||||||
|
("20".to_string(), "beIN SPORTS 2".to_string()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// Le flux choisi pour beIN 1 est gardé, beIN 2 complété.
|
||||||
|
r.channel_ids = vec!["10".into()];
|
||||||
|
let labels = channel_labels(&r, &names);
|
||||||
|
assert_eq!(labels[0], ("10".to_string(), "beIN SPORTS 1".to_string()));
|
||||||
|
assert_eq!(labels.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user