Skip to content

Commit 9a6b7e5

Browse files
authored
epg fix (#537)
New Features Aliases can now be individually enabled or disabled to manage active sources. Bug Fixes Improved channel identification accuracy in XMLTV outputs. Style Disabled aliases display with strikethrough text for better visibility.
1 parent 62f7611 commit 9a6b7e5

21 files changed

Lines changed: 132 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
34
## 3.3.0 (2026-01-03)
45

56
## ⚠️ Breaking Changes

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tuliprox"
3-
version = "3.2.53"
3+
version = "3.2.54"
44
edition = "2021"
55
rust-version = "1.89.0"
66

backend/src/api/endpoints/xmltv_api.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ async fn serve_epg_with_rewrites(
197197
let (user_start, user_stop) = apply_user_offset(programme.start, programme.stop, epg_processing_options.offset_minutes);
198198
elem.push_attribute(("start", format_xmltv_time_utc(user_start).as_str()));
199199
elem.push_attribute(("stop", format_xmltv_time_utc(user_stop).as_str()));
200-
elem.push_attribute(("channel", &programme.channel[..]));
200+
elem.push_attribute(("channel", channel.id.as_ref()));
201201
continue_on_err!(writer.write_event_async(Event::Start(elem)).await);
202202

203203
if let Some(title) = &programme.title {
@@ -267,18 +267,18 @@ fn apply_user_offset(start: i64, stop: i64, offset_minutes: i32) -> (i64, i64) {
267267
(user_start, user_end)
268268
}
269269

270-
fn from_programme(stream_id: &Arc<str>, programme: &EpgProgramme, epg_processing_options: &EpgProcessingOptions) -> ShortEpgDto {
270+
fn from_programme(stream_id: &Arc<str>, epg_id: &Arc<str>, programme: &EpgProgramme, epg_processing_options: &EpgProcessingOptions) -> ShortEpgDto {
271271
let (user_start, user_end) = apply_user_offset(programme.start, programme.stop, epg_processing_options.offset_minutes);
272272

273273
ShortEpgDto {
274274
id: Arc::clone(stream_id),
275-
epg_id: Arc::clone(&programme.channel),
275+
epg_id: Arc::clone(epg_id),
276276
title: programme.title.as_ref().map_or_else(String::new, ToString::to_string),
277277
lang: String::new(),
278278
start: format_xmltv_time(user_start),
279279
end: format_xmltv_time(user_end),
280280
description: programme.desc.as_ref().map_or_else(String::new, ToString::to_string),
281-
channel_id: Arc::clone(&programme.channel),
281+
channel_id: Arc::clone(epg_id),
282282
start_timestamp: user_start.to_string(),
283283
stop_timestamp: user_end.to_string(),
284284
stream_id: Arc::clone(stream_id),
@@ -300,9 +300,9 @@ pub async fn serve_short_epg(
300300
let epg_processing_options = get_epg_processing_options(app_state, user, target);
301301
ShortEpgResultDto {
302302
epg_listings: if limit > 0 {
303-
epg_channel.get_programme_with_limit(limit).iter().map(|p| from_programme(&stream_id, p, &epg_processing_options)).collect()
303+
epg_channel.get_programme_with_limit(limit).iter().map(|p| from_programme(&stream_id, channel_id, p, &epg_processing_options)).collect()
304304
} else {
305-
epg_channel.programmes.iter().map(|p| from_programme(&stream_id, p, &epg_processing_options)).collect()
305+
epg_channel.programmes.iter().map(|p| from_programme(&stream_id, channel_id, p, &epg_processing_options)).collect()
306306
},
307307
}
308308
} else {

backend/src/api/model/active_provider_manager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ impl ActiveProviderManager {
6767
}
6868

6969
fn get_config_inputs(cfg: &AppConfig) -> Vec<Arc<ConfigInput>> {
70-
cfg.sources.load().inputs.iter().map(Arc::clone).collect()
70+
cfg.sources.load().inputs.iter().filter(|i| i.enabled).map(Arc::clone).collect()
7171
}
7272

7373
fn get_grace_options(cfg: &AppConfig) -> (u64, u64) {

backend/src/api/model/provider_lineup_manager.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ impl MultiProviderLineup {
247247
) -> Self {
248248
let input_connection = get_or_create_provider_connection(provider_connections, &cfg_input.name);
249249
let mut inputs = vec![ProviderConfigWrapper::new(ProviderConfig::new(cfg_input, input_connection, Arc::clone(connection_change)))];
250-
if let Some(aliases) = &cfg_input.aliases {
250+
if let Some(aliases) = cfg_input.get_enabled_aliases() {
251251
for alias in aliases {
252252
let alias_connection = get_or_create_provider_connection(provider_connections, &alias.name);
253253
inputs.push(ProviderConfigWrapper::new(ProviderConfig::new_alias(
@@ -540,7 +540,7 @@ impl ProviderLineupManager {
540540
event_manager.send_provider_event(name, connections);
541541
});
542542

543-
if cfg_input.aliases.as_ref().is_some_and(|a| !a.is_empty()) {
543+
if cfg_input.has_enabled_aliases() {
544544
ProviderLineup::Multi(MultiProviderLineup::new(cfg_input, provider_connections, &on_connection_change))
545545
} else {
546546
let connection = get_or_create_provider_connection(provider_connections, &cfg_input.name);
@@ -575,6 +575,7 @@ impl ProviderLineupManager {
575575
};
576576

577577
if a_alias.max_connections != b_alias.max_connections
578+
|| a_alias.enabled != b_alias.enabled
578579
|| a_alias.priority != b_alias.priority
579580
|| a_alias.username != b_alias.username
580581
|| a_alias.password != b_alias.password

backend/src/api/panel_api.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,7 @@ async fn patch_source_yml_add_alias(
13641364
priority: 0,
13651365
max_connections: 1,
13661366
exp_date,
1367+
enabled: true,
13671368
};
13681369

13691370
input.upsert_alias(alias)?;
@@ -1632,6 +1633,7 @@ fn apply_sources_yml_patches(
16321633
priority: 0,
16331634
max_connections: 1,
16341635
exp_date: *exp_date,
1636+
enabled: true,
16351637
};
16361638
alias.prepare(next_index, &input_type)?;
16371639
aliases.push(alias);

backend/src/model/config/input.rs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
use crate::model::{macros, EpgConfig, PanelApiConfig};
2+
use crate::repository::get_csv_file_path;
23
use chrono::Utc;
34
use log::warn;
5+
use shared::check_input_credentials;
46
use shared::error::TuliproxError;
57
use shared::model::{ConfigInputAliasDto, ConfigInputDto, ConfigInputOptionsDto, InputFetchMethod, InputType, StagedInputDto};
68
use shared::utils::{get_credentials_from_url, Internable};
79
use shared::{check_input_connections, info_err_res, write_if_some};
8-
use shared::check_input_credentials;
910
use std::collections::HashMap;
1011
use std::fmt;
1112
use std::path::PathBuf;
1213
use std::sync::Arc;
1314
use url::Url;
14-
use crate::repository::get_csv_file_path;
1515

1616
#[allow(clippy::struct_excessive_bools)]
1717
#[derive(Debug, Clone)]
@@ -105,6 +105,7 @@ pub struct ConfigInputAlias {
105105
pub priority: i16,
106106
pub max_connections: u16,
107107
pub exp_date: Option<i64>,
108+
pub enabled: bool,
108109
}
109110

110111
macros::from_impl!(ConfigInputAlias);
@@ -119,6 +120,7 @@ impl From<&ConfigInputAliasDto> for ConfigInputAlias {
119120
priority: dto.priority,
120121
max_connections: dto.max_connections,
121122
exp_date: dto.exp_date,
123+
enabled: dto.enabled,
122124
}
123125
}
124126
}
@@ -165,6 +167,15 @@ impl ConfigInput {
165167
self.enabled = false;
166168
}
167169

170+
if let Some(aliases) = &mut self.aliases {
171+
for alias in aliases {
172+
if is_input_expired(alias.exp_date) {
173+
warn!("Account {} expired for provider: {}", alias.username.as_ref().map_or("?", |s| s.as_str()), alias.name);
174+
alias.enabled = false;
175+
}
176+
}
177+
}
178+
168179
if let Some(panel_api) = &mut self.panel_api {
169180
panel_api.prepare()?;
170181
}
@@ -210,15 +221,20 @@ impl ConfigInput {
210221
}
211222

212223
if !aliases.is_empty() {
213-
let mut first = aliases.remove(0);
214-
self.id = first.id;
215-
self.username = first.username.take();
216-
self.password = first.password.take();
217-
self.url = first.url.trim().to_string();
218-
self.max_connections = first.max_connections;
219-
self.priority = first.priority;
220-
if self.name.is_empty() {
221-
self.name.clone_from(&first.name);
224+
if let Some(index) = aliases.iter().position(|alias| alias.enabled) {
225+
let mut first = aliases.remove(index);
226+
self.id = first.id;
227+
self.username = first.username.take();
228+
self.password = first.password.take();
229+
self.url = first.url.trim().to_string();
230+
self.max_connections = first.max_connections;
231+
self.priority = first.priority;
232+
self.enabled = first.enabled;
233+
if self.name.is_empty() {
234+
self.name.clone_from(&first.name);
235+
}
236+
} else {
237+
self.enabled = false;
222238
}
223239
}
224240
}
@@ -254,6 +270,23 @@ impl ConfigInput {
254270
cache_duration_seconds: self.cache_duration_seconds,
255271
}
256272
}
273+
274+
pub fn has_enabled_aliases(&self) -> bool {
275+
self.aliases
276+
.as_ref()
277+
.is_some_and(|aliases| aliases.iter().any(|a| a.enabled))
278+
}
279+
280+
pub fn get_enabled_aliases(&self) -> Option<Vec<&ConfigInputAlias>> {
281+
self.aliases.as_ref().map_or(None, |aliases| {
282+
let result: Vec<_> = aliases.iter().filter(|alias| alias.enabled).collect();
283+
if result.is_empty() {
284+
None
285+
} else {
286+
Some(result)
287+
}
288+
})
289+
}
257290
}
258291

259292
macros::from_impl!(ConfigInput);

backend/src/model/xmltv.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ fn filter_channels_and_programmes(
194194
) {
195195
let mut prog_map: HashMap<Arc<str>, Vec<EpgProgramme>> = HashMap::new();
196196
for prog in programmes.drain(..) {
197-
prog_map.entry(prog.channel.clone()).or_default().push(prog);
197+
prog_map.entry(prog.get_transient_channel_id().clone()).or_default().push(prog);
198198
}
199199

200200
for channel in channels.iter_mut() {

backend/src/repository/alias_repository.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const FIELD_NAME: &str = "name";
2323
const FIELD_USERNAME: &str = "username";
2424
const FIELD_PASSWORD: &str = "password";
2525
const FIELD_EXP_DATE: &str = "exp_date";
26+
const FIELD_ENABLED: &str = "enabled";
2627
const FIELD_UNKNOWN: &str = "?";
2728
const DEFAULT_COLUMNS: &[&str] = &[
2829
FIELD_URL,
@@ -32,6 +33,7 @@ const DEFAULT_COLUMNS: &[&str] = &[
3233
FIELD_USERNAME,
3334
FIELD_PASSWORD,
3435
FIELD_EXP_DATE,
36+
FIELD_ENABLED
3537
];
3638
const CSV_EXTENSION: &str = ".csv";
3739

@@ -108,6 +110,16 @@ fn csv_assign_mandatory_fields(alias: &mut ConfigInputAliasDto, input_type: Inpu
108110
}
109111
}
110112

113+
fn str_to_bool(val: &str) -> bool {
114+
if val.is_empty() || val == "1" {
115+
return true;
116+
}
117+
if val == "0" || val.eq_ignore_ascii_case("f") || val.eq_ignore_ascii_case("false") {
118+
return false;
119+
}
120+
true
121+
}
122+
111123
fn csv_assign_config_input_column(
112124
config_input: &mut ConfigInputAliasDto,
113125
header: &str,
@@ -143,6 +155,9 @@ fn csv_assign_config_input_column(
143155
None
144156
});
145157
}
158+
FIELD_ENABLED => {
159+
config_input.enabled = str_to_bool(value);
160+
}
146161
_ => {}
147162
}
148163
}
@@ -180,6 +195,7 @@ pub fn csv_read_inputs_from_reader(
180195
FIELD_USERNAME => FIELD_USERNAME,
181196
FIELD_PASSWORD => FIELD_PASSWORD,
182197
FIELD_EXP_DATE => FIELD_EXP_DATE,
198+
FIELD_ENABLED => FIELD_ENABLED,
183199
_ => {
184200
error!("Field {s} is unsupported for csv input");
185201
FIELD_UNKNOWN
@@ -199,6 +215,7 @@ pub fn csv_read_inputs_from_reader(
199215
priority: 0,
200216
max_connections: 1,
201217
exp_date: None,
218+
enabled: true,
202219
};
203220

204221
let columns: Vec<&str> = line.split(CSV_SEPARATOR).collect();
@@ -276,6 +293,8 @@ async fn csv_write_input_to_path(
276293
content.push(CSV_SEPARATOR);
277294
content.push_str(FIELD_URL);
278295
content.push(CSV_SEPARATOR);
296+
content.push_str(FIELD_ENABLED);
297+
content.push(CSV_SEPARATOR);
279298
content.push_str(FIELD_MAX_CON);
280299
content.push(CSV_SEPARATOR);
281300
content.push_str(FIELD_PRIO);
@@ -292,6 +311,8 @@ async fn csv_write_input_to_path(
292311
content.push(CSV_SEPARATOR);
293312
content.push_str(&alias.url);
294313
content.push(CSV_SEPARATOR);
314+
content.push_str(if alias.enabled { "1" } else {"0"} );
315+
content.push(CSV_SEPARATOR);
295316
content.push_str(&alias.max_connections.to_string());
296317
content.push(CSV_SEPARATOR);
297318
content.push_str(&alias.priority.to_string());
@@ -352,6 +373,7 @@ pub async fn csv_patch_batch_append(
352373
priority: 0,
353374
max_connections: 1,
354375
exp_date,
376+
enabled: true,
355377
};
356378
aliases.push(alias);
357379

0 commit comments

Comments
 (0)