mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-21 15:22:20 +02:00
Merge pull request #392 from euzu/feature/refactorings
Some refactorings
This commit is contained in:
@@ -218,7 +218,7 @@ Attributes:
|
||||
- `throttle` Allowed units are `KB/s`,`MB/s`,`KiB/s`,`MiB/s`,`kbps`,`mbps`,`Mibps`. Default unit is `kbps`
|
||||
- `grace_period_millis` default set to 300 milliseconds.
|
||||
- `grace_period_timeout_secs` default set to 2 seconds.
|
||||
- `geopip` is for resolving ip addresses to country names.
|
||||
- `geoip` is for resolving IP addresses to country names.
|
||||
|
||||
##### 1.6.1.1 `retry`
|
||||
If set to `true` on connection loss to provider, the stream will be reconnected.
|
||||
@@ -279,8 +279,8 @@ It has 2 attributes:
|
||||
url: <the url>
|
||||
```
|
||||
|
||||
The `url` is optional and default vaue is: `https://raw.githubusercontent.com/sapics/ip-location-db/refs/heads/main/asn-country/asn-country-ipv4.csv`
|
||||
The format is csv with 3 columns `range_start,range_end,country_code`
|
||||
The `url` is optional; default value: `https://raw.githubusercontent.com/sapics/ip-location-db/refs/heads/main/asn-country/asn-country-ipv4.csv`
|
||||
The format is CSV with 3 columns: `range_start,range_end,country_code`.
|
||||
|
||||
Example:
|
||||
```csv
|
||||
@@ -1518,7 +1518,7 @@ user:
|
||||
status: Active
|
||||
```
|
||||
|
||||
If yu use a reverse proxy in fron of Tuliprox, dont forget to forward
|
||||
If you use a reverse proxy in front of Tuliprox, don’t forget to forward:
|
||||
- `X-Real-IP`
|
||||
- `X-Forwarded-For`
|
||||
|
||||
|
||||
@@ -1144,7 +1144,7 @@ async fn fetch_resource_with_retry(
|
||||
build_stream_response(app_state, resource_url, response).await,
|
||||
);
|
||||
}
|
||||
// Retry only for 400, 408, 425, 429 and all 5xx statuses
|
||||
// Retry only for 408, 425, 429 and all 5xx statuses
|
||||
let should_retry = status.is_server_error()
|
||||
|| matches!(
|
||||
status,
|
||||
|
||||
@@ -107,7 +107,7 @@ async fn geoip_update(axum::extract::State(app_state): axum::extract::State<Arc<
|
||||
},
|
||||
(None, Some(err)) => {
|
||||
error!("Failed to process geoip db: {err}");
|
||||
axum::http::StatusCode::NOT_FOUND.into_response()
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
},
|
||||
_ => {
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
|
||||
@@ -60,10 +60,16 @@ async fn playlist_update(
|
||||
let process_targets = app_state.app_config.sources.load().validate_targets(user_targets.as_ref());
|
||||
match process_targets {
|
||||
Ok(valid_targets) => {
|
||||
let http_client = Arc::clone(&app_state.http_client.load());
|
||||
let app_config = Arc::clone(&app_state.app_config);
|
||||
let event_manager = Arc::clone(&app_state.event_manager);
|
||||
let playlist_state = Arc::clone(&app_state.playlists);
|
||||
playlist::exec_processing(Arc::clone(&app_state.http_client.load()), app_config, Arc::new(valid_targets), Some(event_manager), Some(playlist_state)).await;
|
||||
let valid_targets = Arc::new(valid_targets);
|
||||
tokio::spawn({
|
||||
async move {
|
||||
playlist::exec_processing(http_client, app_config, valid_targets, Some(event_manager), Some(playlist_state)).await;
|
||||
}
|
||||
});
|
||||
axum::http::StatusCode::ACCEPTED.into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -219,7 +219,17 @@ async fn xtream_player_api_stream(
|
||||
stream_req: ApiStreamRequest<'_>,
|
||||
) -> impl IntoResponse + Send {
|
||||
|
||||
debug!("Stream Request {stream_req:?} - {req_headers:?}");
|
||||
// if log_enabled!(log::Level::Debug) {
|
||||
// debug!(
|
||||
// "Stream request ctx={} user={} stream_id={} action_path={}",
|
||||
// stream_req.context,
|
||||
// sanitize_sensitive_info(stream_req.username),
|
||||
// sanitize_sensitive_info(stream_req.stream_id),
|
||||
// sanitize_sensitive_info(stream_req.action_path),
|
||||
// );
|
||||
// let message = format!("Client Request headers {req_headers:?}");
|
||||
// debug!("{}", sanitize_sensitive_info(&message));
|
||||
// }
|
||||
|
||||
if log_enabled!(log::Level::Debug) {
|
||||
let message = format!("Client Request headers {req_headers:?}");
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::model::{AppConfig, HdHomeRunDeviceConfig};
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use log::{error, info, trace};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, Read};
|
||||
use std::io::Cursor;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -34,31 +34,43 @@ mod packet {
|
||||
|
||||
// --- UDP Discovery Logic ---
|
||||
|
||||
fn write_tlv_u32(buf: &mut BytesMut, tag: u8, value: u32) {
|
||||
fn write_tlv_u8(buf: &mut bytes::BytesMut, tag: u8, value: u8) {
|
||||
buf.put_u8(tag);
|
||||
buf.put_u8(4);
|
||||
buf.put_u32(value);
|
||||
}
|
||||
|
||||
fn write_tlv_u8(buf: &mut BytesMut, tag: u8, value: u8) {
|
||||
buf.put_u8(tag);
|
||||
buf.put_u8(1);
|
||||
write_tlv_length(buf, 1);
|
||||
buf.put_u8(value);
|
||||
}
|
||||
|
||||
fn write_tlv_str(buf: &mut BytesMut, tag: u8, value: &str) {
|
||||
let bytes = value.as_bytes();
|
||||
fn write_tlv_u32(buf: &mut bytes::BytesMut, tag: u8, value: u32) {
|
||||
buf.put_u8(tag);
|
||||
if bytes.len() < 0x80 {
|
||||
buf.put_u8(u8::try_from(bytes.len()).unwrap_or(0));
|
||||
} else {
|
||||
let len = u16::try_from(bytes.len()).unwrap_or(0);
|
||||
let byte_first = 0x80 | ((len & 0x7F) as u8);
|
||||
let byte_second = ((len >> 7) & 0xFF) as u8;
|
||||
buf.put_u8(byte_first);
|
||||
buf.put_u8(byte_second);
|
||||
write_tlv_length(buf, 4);
|
||||
buf.put_u32(value);
|
||||
}
|
||||
|
||||
fn write_tlv_str(buf: &mut bytes::BytesMut, tag: u8, value: &str) {
|
||||
let bytes = value.as_bytes();
|
||||
if bytes.len() > 0x7FFF {
|
||||
// maximum length for 15-bit TLV = 32767
|
||||
log::warn!("TLV string too long, truncating to 32767 bytes");
|
||||
}
|
||||
let len = std::cmp::min(bytes.len(), 0x7FFF);
|
||||
buf.put_u8(tag);
|
||||
write_tlv_length(buf, len);
|
||||
buf.put_slice(&bytes[..len]);
|
||||
}
|
||||
|
||||
// helper function for variable-length TLV
|
||||
fn write_tlv_length(buf: &mut bytes::BytesMut, len: usize) {
|
||||
if len <= 0x7F {
|
||||
// ≤ 127 → 1 Byte length
|
||||
buf.put_u8(u8::try_from(len).unwrap_or(0xFF));
|
||||
} else {
|
||||
// ≥ 128 → 2-byte length with the highest bit set
|
||||
let len = u16::try_from(len).unwrap_or(0); // safe, we limit it above to 0x7FFF
|
||||
let first = 0x80 | ((len & 0x7F) as u8);
|
||||
let second = ((len >> 7) & 0xFF) as u8;
|
||||
buf.put_u8(first);
|
||||
buf.put_u8(second);
|
||||
}
|
||||
buf.put_slice(bytes);
|
||||
}
|
||||
|
||||
fn build_discover_response(device: &HdHomeRunDeviceConfig, server_host: &str) -> Vec<u8> {
|
||||
@@ -96,7 +108,11 @@ fn build_discover_response(device: &HdHomeRunDeviceConfig, server_host: &str) ->
|
||||
|
||||
let mut response = BytesMut::new();
|
||||
response.put_u16(packet::HDHOMERUN_TYPE_DISCOVER_RSP);
|
||||
response.put_u16(u16::try_from(payload.len()).unwrap_or(0));
|
||||
//response.put_u16(u16::try_from(payload.len()).unwrap_or(0));
|
||||
if let Ok(n) = u16::try_from(payload.len()) { response.put_u16(n) } else {
|
||||
error!("HDHR response payload too large ({} bytes)", payload.len());
|
||||
return Vec::new();
|
||||
}
|
||||
response.put(payload);
|
||||
|
||||
let crc = crc32fast::hash(&response);
|
||||
@@ -107,39 +123,64 @@ fn build_discover_response(device: &HdHomeRunDeviceConfig, server_host: &str) ->
|
||||
|
||||
fn parse_tlv(cursor: &mut Cursor<&[u8]>) -> HashMap<u8, Vec<u8>> {
|
||||
let mut tags = HashMap::new();
|
||||
while cursor.position() < cursor.get_ref().len() as u64 {
|
||||
let mut tag_buf = [0u8; 1];
|
||||
if Read::read_exact(cursor, &mut tag_buf).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut len_buf = [0u8; 1];
|
||||
if Read::read_exact(cursor, &mut len_buf).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let len = if (len_buf[0] & 0x80) == 0 {
|
||||
len_buf[0] as usize
|
||||
} else {
|
||||
let mut second_byte = [0u8; 1];
|
||||
if Read::read_exact(cursor, &mut second_byte).is_err() {
|
||||
break;
|
||||
loop {
|
||||
let pos = match usize::try_from(cursor.position()) {
|
||||
Ok(pos) => pos,
|
||||
Err(_err) => {
|
||||
return tags;
|
||||
}
|
||||
((second_byte[0] as usize) << 7) + ((len_buf[0] & 0x7F) as usize)
|
||||
};
|
||||
|
||||
if pos >= cursor.get_ref().len() {
|
||||
break;
|
||||
}
|
||||
let mut tag_buf = [0u8; 1];
|
||||
if std::io::Read::read_exact(cursor, &mut tag_buf).is_err() {
|
||||
break;
|
||||
}
|
||||
let tag = tag_buf[0];
|
||||
|
||||
if cursor.get_ref().len() < usize::try_from(cursor.position()).unwrap_or(0) + len {
|
||||
// // 2-Byte length (big-endian)
|
||||
// let mut len_buf = [0u8; 2];
|
||||
// if std::io::Read::read_exact(cursor, &mut len_buf).is_err() {
|
||||
// break;
|
||||
// }
|
||||
// let len = u16::from_be_bytes(len_buf) as usize;
|
||||
|
||||
// Variable-length TLV decoding
|
||||
let mut first_len_byte = [0u8; 1];
|
||||
if std::io::Read::read_exact(cursor, &mut first_len_byte).is_err() {
|
||||
break;
|
||||
}
|
||||
let len = if first_len_byte[0] & 0x80 == 0 {
|
||||
// Single-byte length (≤127)
|
||||
first_len_byte[0] as usize
|
||||
} else {
|
||||
// Two-byte length (≥128)
|
||||
let mut second_len_byte = [0u8; 1];
|
||||
if std::io::Read::read_exact(cursor, &mut second_len_byte).is_err() {
|
||||
break;
|
||||
}
|
||||
let low_bits = (first_len_byte[0] & 0x7F) as usize;
|
||||
let high_bits = (second_len_byte[0] as usize) << 7;
|
||||
low_bits | high_bits
|
||||
};
|
||||
|
||||
// Check for incomplete TLV
|
||||
let remaining = cursor.get_ref().len() as u64 - cursor.position();
|
||||
if remaining < len as u64 {
|
||||
break; // incomplete or invalid TLV record
|
||||
}
|
||||
|
||||
let mut val_buf = vec![0u8; len];
|
||||
if std::io::Read::read_exact(cursor, &mut val_buf).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut val_buf = vec![0; len];
|
||||
if Read::read_exact(cursor, &mut val_buf).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
tags.insert(tag_buf[0], val_buf);
|
||||
tags.insert(tag, val_buf);
|
||||
}
|
||||
|
||||
tags
|
||||
}
|
||||
|
||||
@@ -164,9 +205,27 @@ async fn proprietary_discover_loop(
|
||||
continue;
|
||||
}
|
||||
|
||||
// let mut cursor = Cursor::new(data);
|
||||
// let msg_type = cursor.get_u16();
|
||||
// let _msg_len = cursor.get_u16();
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
let msg_type = cursor.get_u16();
|
||||
let _msg_len = cursor.get_u16();
|
||||
let msg_len = cursor.get_u16() as usize;
|
||||
|
||||
// Validate total size: header (4) + payload (msg_len) + CRC (4)
|
||||
if data.len() < 4 + msg_len + 4 {
|
||||
trace!("Short HDHR discovery packet from {remote_addr}");
|
||||
continue;
|
||||
}
|
||||
let payload_end = 4 + msg_len;
|
||||
let (framed, crc_tail) = data.split_at(payload_end);
|
||||
let received_crc = u32::from_le_bytes(crc_tail[..4].try_into().unwrap_or_default());
|
||||
if crc32fast::hash(framed) != received_crc {
|
||||
trace!("Invalid CRC in HDHR discovery packet from {remote_addr}");
|
||||
continue;
|
||||
}
|
||||
let mut cursor = Cursor::new(&framed[4..]); // parse TLV over payload only
|
||||
|
||||
if msg_type == packet::HDHOMERUN_TYPE_DISCOVER_REQ {
|
||||
let tags = parse_tlv(&mut cursor);
|
||||
@@ -187,6 +246,10 @@ async fn proprietary_discover_loop(
|
||||
};
|
||||
if should_reply {
|
||||
let response = build_discover_response(device, &server_host);
|
||||
if response.is_empty() {
|
||||
error!("Failed to build discovery response for device '{}'", device.name);
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = socket.send_to(&response, remote_addr).await {
|
||||
error!("Failed to send proprietary discovery response to {remote_addr}: {e}");
|
||||
} else {
|
||||
@@ -241,8 +304,18 @@ async fn handle_tcp_connection(
|
||||
}
|
||||
|
||||
async fn process_getset_request(request: &[u8], app_state: &Arc<AppState>) -> Vec<u8> {
|
||||
if request.len() < 8 {
|
||||
error!("HDHR GET/SET request too short");
|
||||
return Vec::new();
|
||||
}
|
||||
let (data, crc_bytes) = request.split_at(request.len() - 4);
|
||||
let received_crc = u32::from_le_bytes(crc_bytes.try_into().unwrap());
|
||||
|
||||
let Ok(crc_array) = crc_bytes.try_into() else {
|
||||
error!("Invalid CRC bytes in HDHR GET/SET request");
|
||||
return Vec::new();
|
||||
};
|
||||
let received_crc = u32::from_le_bytes(crc_array);
|
||||
|
||||
if crc32fast::hash(data) != received_crc {
|
||||
error!("Invalid CRC in HDHomeRun GET/SET request");
|
||||
return Vec::new();
|
||||
@@ -278,10 +351,7 @@ async fn process_getset_request(request: &[u8], app_state: &Arc<AppState>) -> Ve
|
||||
if let Ok(tuner_index) = rest[..end].parse::<usize>() {
|
||||
let active_streams = app_state.active_users.active_streams().await;
|
||||
let status_str = if let Some(stream_info) = active_streams.get(tuner_index) {
|
||||
format!(
|
||||
"ch={} lock=8vsb ss=98 snq=80 seq=90 bps=12345678 pps=1234",
|
||||
stream_info.channel.title
|
||||
)
|
||||
format!("ch={} lock=8vsb ss=98 snq=80 seq=90 bps=12345678 pps=1234", stream_info.channel.title)
|
||||
} else {
|
||||
"ch=none lock=none ss=0 snq=0 seq=0 bps=0 pps=0".to_string()
|
||||
};
|
||||
@@ -311,25 +381,22 @@ async fn process_getset_request(request: &[u8], app_state: &Arc<AppState>) -> Ve
|
||||
}
|
||||
s if s.starts_with("/tuner") && s.ends_with("/lockkey") => {
|
||||
let err_msg = "ERROR: resource locked";
|
||||
write_tlv_str(
|
||||
&mut response_payload,
|
||||
packet::HDHOMERUN_TAG_ERROR_MESSAGE,
|
||||
err_msg,
|
||||
);
|
||||
write_tlv_str(&mut response_payload, packet::HDHOMERUN_TAG_ERROR_MESSAGE, err_msg);
|
||||
}
|
||||
_ => {
|
||||
write_tlv_str(
|
||||
&mut response_payload,
|
||||
packet::HDHOMERUN_TAG_GETSET_VALUE,
|
||||
"",
|
||||
);
|
||||
write_tlv_str(&mut response_payload,packet::HDHOMERUN_TAG_GETSET_VALUE,"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut response = BytesMut::new();
|
||||
response.put_u16(packet::HDHOMERUN_TYPE_GETSET_RSP);
|
||||
response.put_u16(u16::try_from(response_payload.len()).unwrap_or(0));
|
||||
if let Ok(n) = u16::try_from(response_payload.len()) {
|
||||
response.put_u16(n);
|
||||
} else {
|
||||
error!("HDHR GET/SET response payload too large ({} bytes)", response_payload.len());
|
||||
return Vec::new();
|
||||
}
|
||||
response.put(response_payload);
|
||||
|
||||
let crc = crc32fast::hash(&response);
|
||||
|
||||
@@ -52,7 +52,7 @@ async fn ssdp_task_loop(socket: UdpSocket, app_config: Arc<AppConfig>, server_ho
|
||||
"upnp:rootdevice",
|
||||
"ssdp:all",
|
||||
];
|
||||
if !supported.contains(&st.as_str()) && st != "ssdp:all" { continue; }
|
||||
if !supported.contains(&st.as_str()) { continue; }
|
||||
// Randomized delay per MX
|
||||
let delay_ms = (fastrand::u64(0..=mx*1000)).min(2000);
|
||||
if delay_ms > 0 { tokio::time::sleep(Duration::from_millis(delay_ms)).await; }
|
||||
|
||||
@@ -70,14 +70,19 @@ fn create_shared_data(
|
||||
let config = app_config.config.load();
|
||||
|
||||
let use_geoip = config.is_geoip_enabled();
|
||||
let geoip = if use_geoip {
|
||||
let geoip = if use_geoip {
|
||||
let path = get_geoip_path(&config.working_dir);
|
||||
let _file_lock = app_config.file_locks.read_lock(&path);
|
||||
let geoip = GeoIp::load(&path).ok();
|
||||
if geoip.is_some() {
|
||||
info!("GeoIp db loaded");
|
||||
match GeoIp::load(&path) {
|
||||
Ok(db) => {
|
||||
info!("GeoIp db loaded");
|
||||
Arc::new(ArcSwapOption::from(Some(Arc::new(db))))
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to load GeoIp db: {err}");
|
||||
Arc::new(ArcSwapOption::from(None))
|
||||
}
|
||||
}
|
||||
Arc::new(ArcSwapOption::from_pointee(geoip))
|
||||
} else {
|
||||
Arc::new(ArcSwapOption::from(None))
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ impl ProviderConnectionGuard {
|
||||
|
||||
fn send_release(&self, config: &Arc<ProviderConfig>) {
|
||||
let provider_config = Arc::clone(config);
|
||||
if let Err(_err) = &self.release_tx.send(Arc::clone(config)) {
|
||||
if let Err(_err) = &self.release_tx.send(provider_config.clone()) {
|
||||
// Fallback
|
||||
tokio::spawn(async move {
|
||||
provider_config.release().await;
|
||||
@@ -536,8 +536,8 @@ impl ProviderLineupManager {
|
||||
let on_connection_change: ProviderConnectionChangeCallback = Arc::new(move |_name: &str, connections: usize| {
|
||||
let connection_change_sender = connection_change_sender.clone();
|
||||
let provider_cfg_name = cfg_name.clone();
|
||||
if let Err(err) = connection_change_sender.send((provider_cfg_name, connections)) {
|
||||
error!("Failed to send connection change: {cfg_name}: {connections}, {err}");
|
||||
if let Err(err) = connection_change_sender.send((provider_cfg_name.clone(), connections)) {
|
||||
error!("Failed to send connection change: {provider_cfg_name}: {connections}, {err}");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -260,11 +260,11 @@ impl ProviderConfig {
|
||||
|
||||
pub async fn release(&self) {
|
||||
let mut guard = self.connection.write().await;
|
||||
if guard.current_connections == 1 || guard.current_connections > self.max_connections {
|
||||
guard.granted_grace = false;
|
||||
guard.grace_ts = 0;
|
||||
}
|
||||
if guard.current_connections > 0 {
|
||||
if guard.current_connections == 1 && self.max_connections > 1 {
|
||||
guard.granted_grace = false;
|
||||
guard.grace_ts = 0;
|
||||
}
|
||||
modify_connections!(self, guard, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,6 @@ impl SharedStreamState {
|
||||
|
||||
let mut loop_cnt = 0;
|
||||
loop {
|
||||
loop_cnt += 1;
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
@@ -162,16 +161,18 @@ impl SharedStreamState {
|
||||
debug!("Shared stream client send error: {address} {err}");
|
||||
break;
|
||||
}
|
||||
if loop_cnt > YIELD_COUNTER {
|
||||
tokio::task::yield_now().await;
|
||||
loop_cnt = 0;
|
||||
}
|
||||
loop_cnt += 1;
|
||||
if loop_cnt >= YIELD_COUNTER {
|
||||
tokio::task::yield_now().await;
|
||||
loop_cnt = 0;
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
trace!("Client lagged behind. Skipped {skipped} messages. {address}");
|
||||
if loop_cnt > YIELD_COUNTER {
|
||||
tokio::task::yield_now().await;
|
||||
loop_cnt = 0;
|
||||
loop_cnt += 1;
|
||||
if loop_cnt >= YIELD_COUNTER {
|
||||
tokio::task::yield_now().await;
|
||||
loop_cnt = 0;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
|
||||
@@ -151,8 +151,8 @@ where
|
||||
break;
|
||||
}
|
||||
() = &mut signal_closed => {
|
||||
if let Err(_err) = connection_release.send(remote_addr.to_string()) {
|
||||
let addr = remote_addr.to_string();
|
||||
let addr = remote_addr.to_string();
|
||||
if let Err(_err) = connection_release.send(addr.clone()) {
|
||||
user_manager_clone.remove_connection(&addr).await;
|
||||
}
|
||||
debug!("Connection gracefully closed: {remote_addr}");
|
||||
|
||||
@@ -67,7 +67,7 @@ impl Fingerprint {
|
||||
let client_ip_port =format!("{client_ip}:{}", addr.port());
|
||||
|
||||
let ua = user_agent.unwrap_or_else(String::new);
|
||||
let key = format!("{client_ip }{ua}");
|
||||
let key = format!("{client_ip}|{ua}");
|
||||
|
||||
Ok(Fingerprint(key, client_ip_port))
|
||||
}
|
||||
|
||||
@@ -78,9 +78,11 @@ async fn send_pushover_message(client: &Arc<reqwest::Client>, msg: &str, messagi
|
||||
async fn dispatch_send_message(client: &Arc<reqwest::Client>, kind: MsgKind, cfg: Option<&MessagingConfig>, msg: &str, json: bool) {
|
||||
if let Some(messaging) = cfg {
|
||||
if is_enabled(kind, messaging) {
|
||||
send_telegram_message(client, msg, messaging, json).await;
|
||||
send_http_post_request(client, msg, messaging).await;
|
||||
send_pushover_message(client, msg, messaging).await;
|
||||
tokio::join!(
|
||||
send_telegram_message(client, msg, messaging, json),
|
||||
send_http_post_request(client, msg, messaging),
|
||||
send_pushover_message(client, msg, messaging)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn process_group_watch(client: &Arc<reqwest::Client>, cfg: &Config, ta
|
||||
let save_path = path.as_path();
|
||||
let mut changed = false;
|
||||
if path.exists() {
|
||||
if let Some(loaded_tree) = load_watch_tree(&path) {
|
||||
if let Some(loaded_tree) = load_watch_tree(&path).await {
|
||||
// Find elements in set2 but not in set1
|
||||
let added_difference: BTreeSet<String> = new_tree.difference(&loaded_tree).cloned().collect();
|
||||
let removed_difference: BTreeSet<String> = loaded_tree.difference(&new_tree).cloned().collect();
|
||||
@@ -38,7 +38,7 @@ pub async fn process_group_watch(client: &Arc<reqwest::Client>, cfg: &Config, ta
|
||||
changed = true;
|
||||
}
|
||||
if changed {
|
||||
match save_watch_tree(save_path, &new_tree) {
|
||||
match save_watch_tree(save_path, &new_tree).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
error!("failed to write watch_file {}: {}", save_path.to_str().unwrap_or_default(), err);
|
||||
@@ -64,6 +64,7 @@ async fn handle_watch_notification(client: &Arc<reqwest::Client>, cfg: &Config,
|
||||
let added = added.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
|
||||
let removed = removed.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
|
||||
if !added.is_empty() || !removed.is_empty() {
|
||||
|
||||
let changes = WatchChanges {
|
||||
target: target_name.to_string(),
|
||||
group: group_name.to_string(),
|
||||
@@ -77,15 +78,13 @@ async fn handle_watch_notification(client: &Arc<reqwest::Client>, cfg: &Config,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_watch_tree(path: &Path) -> Option<BTreeSet<String>> {
|
||||
std::fs::read(path).map_or(None, |encoded| {
|
||||
let decoded = bincode_deserialize(&encoded[..]).ok()?;
|
||||
Some(decoded)
|
||||
})
|
||||
async fn load_watch_tree(path: &Path) -> Option<BTreeSet<String>> {
|
||||
let encoded = tokio::fs::read(path).await.ok()?;
|
||||
bincode_deserialize(&encoded[..]).ok()
|
||||
}
|
||||
|
||||
fn save_watch_tree(path: &Path, tree: &BTreeSet<String>) -> std::io::Result<()> {
|
||||
async fn save_watch_tree(path: &Path, tree: &BTreeSet<String>) -> std::io::Result<()> {
|
||||
let encoded: Vec<u8> = bincode_serialize(&tree)?;
|
||||
std::fs::write(path, encoded)
|
||||
tokio::fs::write(path, encoded).await
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::repository::playlist_repository::persist_playlist;
|
||||
use crate::utils::debug_if_enabled;
|
||||
use crate::utils::StepMeasure;
|
||||
use deunicode::deunicode;
|
||||
use futures::StreamExt;
|
||||
use log::{debug, error, info, log_enabled, trace, warn, Level};
|
||||
use reqwest::Client;
|
||||
use shared::error::{get_errors_notify_message, notify_err, TuliproxError};
|
||||
@@ -620,17 +621,20 @@ async fn process_epg(processed_fetched_playlists: &mut Vec<FetchedPlaylist<'_>>)
|
||||
(new_epg, new_playlist)
|
||||
}
|
||||
|
||||
async fn process_watch(cfg: &Config, client: &Arc<reqwest::Client>, target: &ConfigTarget, new_playlist: &Vec<PlaylistGroup>) -> bool {
|
||||
async fn process_watch(cfg: &Config, client: &Arc<reqwest::Client>, target: &ConfigTarget, new_playlist: &[PlaylistGroup]) -> bool {
|
||||
if let Some(watches) = &target.watch {
|
||||
if default_as_default().eq_ignore_ascii_case(&target.name) {
|
||||
error!("cant watch a target with no unique name");
|
||||
} else {
|
||||
for pl in new_playlist {
|
||||
if watches.iter().any(|r| r.is_match(&pl.title)) {
|
||||
process_group_watch(client, cfg, &target.name, pl).await;
|
||||
}
|
||||
}
|
||||
error!("can't watch a target with no unique name");
|
||||
return false;
|
||||
}
|
||||
|
||||
futures::stream::iter(
|
||||
new_playlist
|
||||
.iter()
|
||||
.filter(|pl| watches.iter().any(|r| r.is_match(&pl.title)))
|
||||
.map(|pl| process_group_watch(client, cfg, &target.name, pl))
|
||||
).for_each_concurrent(16, |f| f).await;
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -440,8 +440,6 @@ pub async fn xtream_get_item_for_stream_id(
|
||||
PlaylistItemType::Series => {
|
||||
if let Ok(mut item) = xtream_read_series_item_for_stream_id(app_config, mapping.parent_virtual_id, &storage_path) {
|
||||
item.provider_id = mapping.provider_id;
|
||||
|
||||
|
||||
Ok(item)
|
||||
} else {
|
||||
xtream_read_item_for_stream_id(app_config, virtual_id, &storage_path, XtreamCluster::Series)
|
||||
|
||||
@@ -29,7 +29,9 @@ impl GeoIp {
|
||||
pub fn import_ipv4_from_csv(&mut self, mut reader: impl BufRead, db_path: &Path) -> std::io::Result<u64> {
|
||||
let mut buf = String::new();
|
||||
|
||||
while reader.read_line(&mut buf)? > 0 {
|
||||
loop {
|
||||
buf.clear();
|
||||
if reader.read_line(&mut buf)? == 0 { break; }
|
||||
let line = buf.trim();
|
||||
if line.is_empty() || line.starts_with('#') { continue; }
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ pub fn ConfigView() -> Html {
|
||||
<div class="tp__config-view__header">
|
||||
<h1>{ translate.t(LABEL_CONFIG) } </h1>
|
||||
{html_if!(config_ctx.config.is_some_and(|c| c.config.is_geoip_enabled()), {
|
||||
<TextButton class="tertiary" name="save_config"
|
||||
<TextButton class="tertiary" name="update_geo_ip"
|
||||
icon="Refresh"
|
||||
title={ translate.t(LABEL_UPDATE_GEOIP)}
|
||||
onclick={handle_update_geoip}></TextButton>
|
||||
|
||||
@@ -30,7 +30,7 @@ const HEADERS: [&str; 12] = [
|
||||
"LABEL.DURATION"
|
||||
];
|
||||
|
||||
pub fn format_duration(seconds: u64) -> String {
|
||||
fn format_duration(seconds: u64) -> String {
|
||||
let hours = seconds / 3600;
|
||||
let minutes = (seconds % 3600) / 60;
|
||||
let seconds = seconds % 60;
|
||||
@@ -68,9 +68,8 @@ pub fn StreamsTable(props: &StreamsTableProps) -> Html {
|
||||
|
||||
|
||||
use_effect_with((), move |_| {
|
||||
Interval::new(1000, || {
|
||||
update_timestamps();
|
||||
}).forget();
|
||||
let interval = Interval::new(1000, update_timestamps);
|
||||
move || drop(interval)
|
||||
});
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -22,5 +22,6 @@ zeroize = "1"
|
||||
chrono = "0.4.42"
|
||||
bytes = "1"
|
||||
ciborium = "0.2.2"
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = "0.3.81"
|
||||
|
||||
@@ -9,3 +9,9 @@ pub struct GeoIpConfigDto {
|
||||
#[serde(default = "default_geoip_url")]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl GeoIpConfigDto {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.enabled && self.url.trim().is_empty()
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ impl ReverseProxyConfigDto {
|
||||
&& (self.stream.is_none() || self.stream.as_ref().is_some_and(|s| s.is_empty()))
|
||||
&& (self.cache.is_none() || self.cache.as_ref().is_some_and(|c| c.is_empty()))
|
||||
&& (self.rate_limit.is_none() || self.rate_limit.as_ref().is_some_and(|r| r.is_empty()))
|
||||
&& (self.geoip.is_none() || self.geoip.as_ref().is_some_and(|g| g.is_empty()))
|
||||
}
|
||||
|
||||
pub fn clean(&mut self) {
|
||||
@@ -39,6 +40,9 @@ impl ReverseProxyConfigDto {
|
||||
if self.rate_limit.as_ref().is_some_and(|s| s.is_empty()) {
|
||||
self.rate_limit = None;
|
||||
}
|
||||
if self.geoip.as_ref().is_some_and(|g| g.is_empty()) {
|
||||
self.geoip = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(&mut self, working_dir: &str) -> Result<(), TuliproxError> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::model::{M3uPlaylistItem, PlaylistEntry, PlaylistItemType, XtreamCluster, XtreamPlaylistItem};
|
||||
use crate::utils::{current_time_secs, StringExt};
|
||||
use crate::utils::{current_time_secs, longest};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct StreamChannel {
|
||||
@@ -21,7 +21,7 @@ impl XtreamPlaylistItem {
|
||||
item_type: self.item_type,
|
||||
cluster: self.xtream_cluster,
|
||||
group: self.group.clone(),
|
||||
title: String::longest(self.title.as_str(), self.name.as_str()).to_string(),
|
||||
title: longest(self.title.as_str(), self.name.as_str()).to_string(),
|
||||
url: self.url.clone(),
|
||||
shared: false,
|
||||
}
|
||||
@@ -36,7 +36,7 @@ impl M3uPlaylistItem {
|
||||
item_type: self.item_type,
|
||||
cluster: XtreamCluster::try_from(self.item_type).unwrap_or(XtreamCluster::Live),
|
||||
group: self.group.clone(),
|
||||
title: String::longest(self.title.as_str(), self.name.as_str()).to_string(),
|
||||
title: longest(self.title.as_str(), self.name.as_str()).to_string(),
|
||||
url: self.url.clone(),
|
||||
shared: false,
|
||||
}
|
||||
@@ -49,8 +49,11 @@ pub struct StreamInfo {
|
||||
pub channel: StreamChannel,
|
||||
pub provider: String,
|
||||
pub addr: String,
|
||||
#[serde(default)]
|
||||
pub user_agent: String,
|
||||
#[serde(default)]
|
||||
pub ts: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub country: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -127,14 +127,8 @@ pub fn humanize_snake_case(s: &str) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
pub trait StringExt {
|
||||
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str;
|
||||
}
|
||||
|
||||
impl StringExt for String {
|
||||
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
|
||||
if a.len() >= b.len() { a } else { b }
|
||||
}
|
||||
pub fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
|
||||
if a.len() >= b.len() { a } else { b }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user