mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-02 05:52:26 +02:00
* Fix: setting auth web_ui token_ttl_mins = 0 fixed. * Fix: catchup * New: added playlist update log view * Fix: Landing Page New: "Last Page" added for Landing Page selection
670 lines
28 KiB
Rust
670 lines
28 KiB
Rust
use crate::{
|
|
hooks::use_service_context,
|
|
model::{EventMessage, BACKGROUND_TRANSFER_CLIENT_IP, BACKGROUND_TRANSFER_PROVIDER},
|
|
};
|
|
use shared::{
|
|
model::{
|
|
permission::Permission, ActiveUserConnectionChange, DownloadsDelta, DownloadsResponse, FileDownloadDto,
|
|
PlaylistItemType, ProtocolMessage, StatusCheck, StreamChannel, StreamInfo, SystemInfo, TaskKindDto,
|
|
TransferStatusDto, XtreamCluster,
|
|
},
|
|
utils::{current_time_secs, Internable},
|
|
};
|
|
use std::{
|
|
cell::RefCell,
|
|
collections::{BTreeMap, HashSet},
|
|
hash::{DefaultHasher, Hash, Hasher},
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
rc::Rc,
|
|
};
|
|
use yew::{platform::spawn_local, prelude::*};
|
|
|
|
type ServerStatusState =
|
|
(UseStateHandle<RefCell<Option<Rc<StatusCheck>>>>, UseStateHandle<RefCell<Option<Rc<SystemInfo>>>>);
|
|
|
|
fn stream_identity_key(stream: &StreamInfo) -> (SocketAddr, u32) { (stream.addr, stream.uid) }
|
|
|
|
fn find_stream_update_index(streams: &[StreamInfo], updated_stream: &StreamInfo) -> Option<usize> {
|
|
let updated_key = stream_identity_key(updated_stream);
|
|
if let Some(index) = streams.iter().position(|stream| stream_identity_key(stream) == updated_key) {
|
|
return Some(index);
|
|
}
|
|
|
|
if updated_stream.session_token.is_some() && updated_stream.channel.item_type.requires_provider_affinity() {
|
|
if let Some(session_token) = updated_stream.session_token.as_deref() {
|
|
if let Some(index) =
|
|
streams.iter().position(|stream| stream.session_token.as_deref() == Some(session_token))
|
|
{
|
|
return Some(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
if updated_stream.session_token.is_none() {
|
|
return streams.iter().position(|stream| stream.addr == updated_stream.addr && stream.session_token.is_none());
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn dedupe_streams_by_identity(streams: &mut Vec<StreamInfo>) {
|
|
let mut seen = HashSet::new();
|
|
streams.retain(|stream| seen.insert(stream_identity_key(stream)));
|
|
}
|
|
|
|
fn should_keep_preserved_stream_visible(stream: &StreamInfo) -> bool {
|
|
stream.session_token.is_some() && stream.channel.item_type.is_live_adaptive()
|
|
}
|
|
|
|
fn should_keep_stream_when_connections_drop_to_zero(stream: &StreamInfo) -> bool {
|
|
stream.preserved && should_keep_preserved_stream_visible(stream)
|
|
}
|
|
|
|
fn is_running_download(download: &FileDownloadDto) -> bool { download.status == TransferStatusDto::Running }
|
|
|
|
fn download_stream_uid(id: &str) -> u32 {
|
|
let mut hasher = DefaultHasher::new();
|
|
id.hash(&mut hasher);
|
|
let hash = hasher.finish();
|
|
let mixed = (hash ^ (hash >> 32)) as u32;
|
|
mixed.max(1)
|
|
}
|
|
|
|
fn download_stream_addr(uid: u32) -> SocketAddr {
|
|
let octet3 = ((uid >> 8) & 0xff) as u8;
|
|
let octet4 = (uid & 0xff) as u8;
|
|
let port = ((uid >> 16) % u32::from(u16::MAX - 1) + 1) as u16;
|
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 254, octet3, octet4)), port)
|
|
}
|
|
|
|
fn download_task_to_stream_with_ts(download: &FileDownloadDto, ts: u64) -> StreamInfo {
|
|
let uid = download_stream_uid(&download.id);
|
|
let (item_type, cluster, group) = match download.kind {
|
|
TaskKindDto::Download => (PlaylistItemType::Video, XtreamCluster::Video, "Downloads"),
|
|
TaskKindDto::Recording => (PlaylistItemType::Live, XtreamCluster::Live, "Recordings"),
|
|
};
|
|
StreamInfo {
|
|
uid,
|
|
meter_uid: 0,
|
|
username: "background task".to_string(),
|
|
channel: StreamChannel {
|
|
target_id: 0,
|
|
virtual_id: uid,
|
|
provider_id: 0,
|
|
input_name: "".intern(),
|
|
item_type,
|
|
cluster,
|
|
group: group.intern(),
|
|
title: download.title.clone().intern(),
|
|
url: "".intern(),
|
|
shared: false,
|
|
shared_joined_existing: None,
|
|
shared_stream_id: None,
|
|
technical: None,
|
|
epg_channel_id: None,
|
|
epg_reference_ts: None,
|
|
},
|
|
provider: BACKGROUND_TRANSFER_PROVIDER.intern(),
|
|
addr: download_stream_addr(uid),
|
|
client_ip: BACKGROUND_TRANSFER_CLIENT_IP.to_string(),
|
|
user_agent: "Tuliprox download worker".to_string(),
|
|
ts,
|
|
started_at: ts,
|
|
country_code: None,
|
|
session_token: None,
|
|
preserved: false,
|
|
previous_session_id: None,
|
|
}
|
|
}
|
|
|
|
fn preserved_download_stream_ts(existing_streams: &[StreamInfo], download: &FileDownloadDto) -> u64 {
|
|
let uid = download_stream_uid(&download.id);
|
|
existing_streams
|
|
.iter()
|
|
.find(|stream| stream.uid == uid && stream.client_ip == BACKGROUND_TRANSFER_CLIENT_IP)
|
|
.map_or_else(current_time_secs, |stream| stream.ts)
|
|
}
|
|
|
|
fn merge_aux_streams(server_status: &mut StatusCheck, download_streams: &[StreamInfo]) {
|
|
server_status.active_user_streams.retain(|stream| stream.client_ip != BACKGROUND_TRANSFER_CLIENT_IP);
|
|
server_status.active_user_streams.extend(download_streams.iter().cloned());
|
|
dedupe_streams_by_identity(&mut server_status.active_user_streams);
|
|
}
|
|
|
|
fn rebuild_status_with_downloads(
|
|
status_holder: &UseStateHandle<RefCell<Option<Rc<StatusCheck>>>>,
|
|
status_signal: &UseStateHandle<Option<Rc<StatusCheck>>>,
|
|
download_streams: &[StreamInfo],
|
|
) {
|
|
let mut server_status =
|
|
status_holder.borrow().as_ref().map_or_else(StatusCheck::default, |status| (**status).clone());
|
|
merge_aux_streams(&mut server_status, download_streams);
|
|
let new_status = Rc::new(server_status);
|
|
*status_holder.borrow_mut() = Some(Rc::clone(&new_status));
|
|
status_signal.set(Some(new_status));
|
|
}
|
|
|
|
fn apply_downloads_snapshot(download_streams: &mut Vec<StreamInfo>, response: &DownloadsResponse) {
|
|
let previous_streams = download_streams.clone();
|
|
*download_streams = response
|
|
.active
|
|
.iter()
|
|
.filter(|download| is_running_download(download))
|
|
.map(|download| {
|
|
let ts = preserved_download_stream_ts(&previous_streams, download);
|
|
download_task_to_stream_with_ts(download, ts)
|
|
})
|
|
.collect();
|
|
}
|
|
|
|
fn apply_downloads_delta(download_streams: &mut Vec<StreamInfo>, delta: &DownloadsDelta) {
|
|
match delta {
|
|
DownloadsDelta::SnapshotReset(response) => apply_downloads_snapshot(download_streams, response),
|
|
DownloadsDelta::ActivePatched(download) => {
|
|
let uid = download_stream_uid(&download.id);
|
|
if !is_running_download(download) {
|
|
download_streams.retain(|stream| stream.uid != uid);
|
|
return;
|
|
}
|
|
let ts = preserved_download_stream_ts(download_streams, download);
|
|
let stream = download_task_to_stream_with_ts(download, ts);
|
|
if let Some(existing) = download_streams.iter_mut().find(|current| current.uid == stream.uid) {
|
|
*existing = stream;
|
|
} else {
|
|
download_streams.push(stream);
|
|
}
|
|
}
|
|
DownloadsDelta::ActiveCleared => {
|
|
download_streams.clear();
|
|
}
|
|
DownloadsDelta::QueueReplaced { .. } | DownloadsDelta::FinishedReplaced { .. } => {}
|
|
}
|
|
}
|
|
|
|
fn apply_active_user_change(server_status: &mut StatusCheck, event: ActiveUserConnectionChange) {
|
|
match event {
|
|
ActiveUserConnectionChange::Updated(stream_info) => {
|
|
if stream_info.preserved {
|
|
if should_keep_preserved_stream_visible(&stream_info) {
|
|
if let Some(pos) = find_stream_update_index(&server_status.active_user_streams, &stream_info) {
|
|
server_status.active_user_streams[pos] = stream_info;
|
|
} else {
|
|
server_status.active_user_streams.push(stream_info);
|
|
}
|
|
dedupe_streams_by_identity(&mut server_status.active_user_streams);
|
|
} else if let Some(pos) = find_stream_update_index(&server_status.active_user_streams, &stream_info) {
|
|
server_status.active_user_streams.remove(pos);
|
|
}
|
|
return;
|
|
}
|
|
if let Some(pos) = find_stream_update_index(&server_status.active_user_streams, &stream_info) {
|
|
server_status.active_user_streams[pos] = stream_info;
|
|
} else {
|
|
server_status.active_user_streams.push(stream_info);
|
|
}
|
|
dedupe_streams_by_identity(&mut server_status.active_user_streams);
|
|
}
|
|
ActiveUserConnectionChange::Disconnected(addr) => {
|
|
server_status.active_user_streams.retain(|stream_info| stream_info.addr != addr);
|
|
}
|
|
ActiveUserConnectionChange::DisconnectedStream { addr, uid } => {
|
|
server_status.active_user_streams.retain(|stream_info| stream_info.addr != addr || stream_info.uid != uid);
|
|
}
|
|
ActiveUserConnectionChange::Connections(user_count, connections) => {
|
|
server_status.active_users = user_count;
|
|
server_status.active_user_connections = connections;
|
|
if connections == 0 {
|
|
server_status.active_user_streams.retain(should_keep_stream_when_connections_drop_to_zero);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[hook]
|
|
pub fn use_server_status(
|
|
status: UseStateHandle<Option<Rc<StatusCheck>>>,
|
|
system_info: UseStateHandle<Option<Rc<SystemInfo>>>,
|
|
enabled: bool,
|
|
) -> ServerStatusState {
|
|
let services = use_service_context();
|
|
let status_holder = use_state(|| RefCell::new(None::<Rc<StatusCheck>>));
|
|
let system_info_holder = use_state(|| RefCell::new(None::<Rc<SystemInfo>>));
|
|
let download_streams_holder = use_state(|| RefCell::new(Vec::<StreamInfo>::new()));
|
|
|
|
{
|
|
let services_ctx = services.clone();
|
|
let status_signal = status.clone();
|
|
let status_holder_signal = status_holder.clone();
|
|
let download_streams_holder_signal = download_streams_holder.clone();
|
|
let system_info_signal = system_info.clone();
|
|
let system_info_holder_signal = system_info_holder.clone();
|
|
|
|
use_effect_with(enabled, move |enabled| {
|
|
let mut subid: Option<usize> = None;
|
|
|
|
if *enabled {
|
|
let fetch_status: Rc<dyn Fn()> = Rc::new({
|
|
let services_clone = services_ctx.clone();
|
|
move || {
|
|
let services_clone = services_clone.clone();
|
|
spawn_local(async move {
|
|
services_clone.websocket.get_server_status().await;
|
|
if services_clone.auth.has_permission(Permission::DownloadRead) {
|
|
if services_clone.websocket.send_message(ProtocolMessage::DownloadsRequest) {
|
|
return;
|
|
}
|
|
if let Ok(downloads) = services_clone.downloads.get_downloads().await {
|
|
services_clone.event.broadcast(EventMessage::DownloadsUpdate(Rc::new(downloads)));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
let fetch_status_on_ws = Rc::clone(&fetch_status);
|
|
|
|
subid = Some(services_ctx.event.subscribe(move |msg| match msg {
|
|
EventMessage::ServerStatus(server_status) => {
|
|
let mut server_status = (*server_status).clone();
|
|
dedupe_streams_by_identity(&mut server_status.active_user_streams);
|
|
merge_aux_streams(&mut server_status, download_streams_holder_signal.borrow().as_slice());
|
|
let server_status = Rc::new(server_status);
|
|
*status_holder_signal.borrow_mut() = Some(Rc::clone(&server_status));
|
|
status_signal.set(Some(server_status));
|
|
}
|
|
EventMessage::ActiveUser(event) => {
|
|
let mut server_status = {
|
|
if let Some(old_status) = status_holder_signal.borrow().as_ref() {
|
|
(**old_status).clone()
|
|
} else {
|
|
StatusCheck::default()
|
|
}
|
|
};
|
|
apply_active_user_change(&mut server_status, event);
|
|
merge_aux_streams(&mut server_status, download_streams_holder_signal.borrow().as_slice());
|
|
|
|
let new_status = Rc::new(server_status);
|
|
*status_holder_signal.borrow_mut() = Some(Rc::clone(&new_status));
|
|
status_signal.set(Some(new_status));
|
|
}
|
|
EventMessage::ActiveProvider(provider, connections) => {
|
|
let mut server_status = {
|
|
if let Some(old_status) = status_holder_signal.borrow().as_ref() {
|
|
(**old_status).clone()
|
|
} else {
|
|
StatusCheck::default()
|
|
}
|
|
};
|
|
if let Some(treemap) = server_status.active_provider_connections.as_mut() {
|
|
treemap.insert(provider, connections);
|
|
} else {
|
|
let mut treemap = BTreeMap::new();
|
|
treemap.insert(provider, connections);
|
|
server_status.active_provider_connections = Some(treemap);
|
|
}
|
|
let new_status = Rc::new(server_status);
|
|
*status_holder_signal.borrow_mut() = Some(Rc::clone(&new_status));
|
|
status_signal.set(Some(new_status));
|
|
}
|
|
EventMessage::DownloadsUpdate(downloads) => {
|
|
let mut next_download_streams = (*download_streams_holder_signal.borrow()).clone();
|
|
apply_downloads_snapshot(&mut next_download_streams, &downloads);
|
|
*download_streams_holder_signal.borrow_mut() = next_download_streams.clone();
|
|
rebuild_status_with_downloads(&status_holder_signal, &status_signal, &next_download_streams);
|
|
}
|
|
EventMessage::DownloadsDeltaUpdate(delta) => {
|
|
let mut next_download_streams = (*download_streams_holder_signal.borrow()).clone();
|
|
apply_downloads_delta(&mut next_download_streams, &delta);
|
|
*download_streams_holder_signal.borrow_mut() = next_download_streams.clone();
|
|
rebuild_status_with_downloads(&status_holder_signal, &status_signal, &next_download_streams);
|
|
}
|
|
EventMessage::SystemInfoUpdate(system_info) => {
|
|
let info = Rc::new(system_info);
|
|
*system_info_holder_signal.borrow_mut() = Some(Rc::clone(&info));
|
|
system_info_signal.set(Some(info));
|
|
}
|
|
EventMessage::WebSocketStatus(true) => {
|
|
fetch_status_on_ws();
|
|
}
|
|
_ => {}
|
|
}));
|
|
|
|
fetch_status();
|
|
}
|
|
|
|
let services_clone = services_ctx.clone();
|
|
move || {
|
|
if let Some(subid) = subid {
|
|
services_clone.event.unsubscribe(subid);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
(status_holder, system_info_holder)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
apply_active_user_change, apply_downloads_delta, apply_downloads_snapshot, dedupe_streams_by_identity,
|
|
download_task_to_stream_with_ts, find_stream_update_index,
|
|
};
|
|
use shared::{
|
|
model::{
|
|
ActiveUserConnectionChange, DownloadsDelta, DownloadsResponse, FileDownloadDto, PlaylistItemType,
|
|
StreamChannel, StreamInfo, TaskKindDto, TaskPriorityDto, TransferStatusDto, XtreamCluster,
|
|
},
|
|
utils::Internable,
|
|
};
|
|
use std::net::SocketAddr;
|
|
|
|
fn test_stream(uid: u32, addr: &str, session_token: Option<&str>, item_type: PlaylistItemType) -> StreamInfo {
|
|
StreamInfo {
|
|
uid,
|
|
meter_uid: 2,
|
|
username: "user".to_string(),
|
|
channel: StreamChannel {
|
|
target_id: 1,
|
|
virtual_id: 1,
|
|
provider_id: 1,
|
|
item_type,
|
|
cluster: XtreamCluster::Live,
|
|
group: "group".intern(),
|
|
title: "title".intern(),
|
|
url: "http://localhost/live.m3u8".intern(),
|
|
input_name: "input".intern(),
|
|
shared: false,
|
|
shared_joined_existing: None,
|
|
shared_stream_id: None,
|
|
technical: None,
|
|
epg_channel_id: None,
|
|
epg_reference_ts: None,
|
|
},
|
|
provider: "provider".intern(),
|
|
addr: addr.parse::<SocketAddr>().unwrap_or_else(|_| unreachable!()),
|
|
client_ip: "127.0.0.1".to_string(),
|
|
user_agent: "ua".to_string(),
|
|
ts: 1,
|
|
started_at: 1,
|
|
country_code: None,
|
|
session_token: session_token.map(ToOwned::to_owned),
|
|
preserved: false,
|
|
previous_session_id: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_stream_update_index_prefers_adaptive_session_token_over_addr() {
|
|
let existing = test_stream(1, "127.0.0.1:1234", Some("tok-hls"), PlaylistItemType::LiveHls);
|
|
let updated = test_stream(1, "127.0.0.1:5678", Some("tok-hls"), PlaylistItemType::LiveDash);
|
|
|
|
assert_eq!(find_stream_update_index(&[existing], &updated), Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_stream_update_index_prefers_catchup_session_token_over_addr() {
|
|
let existing = test_stream(3, "127.0.0.1:1234", Some("tok-catchup"), PlaylistItemType::Catchup);
|
|
let updated = test_stream(3, "127.0.0.1:5678", Some("tok-catchup"), PlaylistItemType::Catchup);
|
|
|
|
assert_eq!(find_stream_update_index(&[existing], &updated), Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_stream_update_index_matches_exact_render_key_first() {
|
|
let existing = test_stream(2, "127.0.0.1:1234", Some("tok-a"), PlaylistItemType::LiveHls);
|
|
let updated = test_stream(2, "127.0.0.1:1234", Some("tok-b"), PlaylistItemType::LiveDash);
|
|
|
|
assert_eq!(find_stream_update_index(&[existing], &updated), Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_stream_update_index_keeps_socket_bound_streams_with_same_addr_separate() {
|
|
let existing = test_stream(1, "127.0.0.1:1234", Some("tok-live"), PlaylistItemType::Live);
|
|
let updated = test_stream(2, "127.0.0.1:1234", Some("tok-live"), PlaylistItemType::Live);
|
|
|
|
assert_eq!(find_stream_update_index(&[existing], &updated), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dedupe_streams_by_identity_removes_duplicate_render_keys() {
|
|
let mut streams = vec![
|
|
test_stream(2, "127.0.0.1:1234", Some("tok-a"), PlaylistItemType::LiveHls),
|
|
test_stream(2, "127.0.0.1:1234", Some("tok-b"), PlaylistItemType::LiveDash),
|
|
test_stream(2, "127.0.0.1:5678", Some("tok-c"), PlaylistItemType::LiveDash),
|
|
];
|
|
|
|
dedupe_streams_by_identity(&mut streams);
|
|
|
|
assert_eq!(streams.len(), 2);
|
|
assert_eq!(streams[0].addr, "127.0.0.1:1234".parse::<SocketAddr>().unwrap_or_else(|_| unreachable!()));
|
|
assert_eq!(streams[1].addr, "127.0.0.1:5678".parse::<SocketAddr>().unwrap_or_else(|_| unreachable!()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_connections_zero_clears_stale_stream_rows() {
|
|
let mut status = shared::model::StatusCheck {
|
|
active_users: 1,
|
|
active_user_connections: 1,
|
|
active_user_streams: vec![
|
|
test_stream(1, "127.0.0.1:1234", Some("tok-a"), PlaylistItemType::Video),
|
|
test_stream(2, "127.0.0.1:5678", Some("tok-b"), PlaylistItemType::Series),
|
|
],
|
|
..Default::default()
|
|
};
|
|
|
|
apply_active_user_change(&mut status, ActiveUserConnectionChange::Connections(0, 0));
|
|
|
|
assert_eq!(status.active_users, 0);
|
|
assert_eq!(status.active_user_connections, 0);
|
|
assert!(status.active_user_streams.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_connections_zero_keeps_preserved_adaptive_rows_for_ttl_cleanup() {
|
|
let mut preserved = test_stream(1, "127.0.0.1:1234", Some("tok-hls"), PlaylistItemType::LiveHls);
|
|
preserved.preserved = true;
|
|
let non_adaptive = test_stream(2, "127.0.0.1:5678", Some("tok-vod"), PlaylistItemType::Video);
|
|
let mut status = shared::model::StatusCheck {
|
|
active_users: 1,
|
|
active_user_connections: 1,
|
|
active_user_streams: vec![preserved.clone(), non_adaptive],
|
|
..Default::default()
|
|
};
|
|
|
|
apply_active_user_change(&mut status, ActiveUserConnectionChange::Connections(1, 0));
|
|
|
|
assert_eq!(status.active_users, 1);
|
|
assert_eq!(status.active_user_connections, 0);
|
|
assert_eq!(status.active_user_streams, vec![preserved]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_preserved_adaptive_update_stays_visible_for_frontend_ttl_cleanup() {
|
|
let mut preserved = test_stream(1, "127.0.0.1:1234", Some("tok-hls"), PlaylistItemType::LiveHls);
|
|
preserved.preserved = true;
|
|
let other = test_stream(2, "127.0.0.1:5678", Some("tok-other"), PlaylistItemType::LiveHls);
|
|
let mut status = shared::model::StatusCheck {
|
|
active_users: 2,
|
|
active_user_connections: 2,
|
|
active_user_streams: vec![
|
|
test_stream(1, "127.0.0.1:1234", Some("tok-hls"), PlaylistItemType::LiveHls),
|
|
other.clone(),
|
|
],
|
|
..Default::default()
|
|
};
|
|
|
|
apply_active_user_change(&mut status, ActiveUserConnectionChange::Updated(preserved));
|
|
|
|
assert_eq!(status.active_user_streams.len(), 2);
|
|
assert!(status
|
|
.active_user_streams
|
|
.iter()
|
|
.any(|stream| stream.addr == "127.0.0.1:1234".parse::<SocketAddr>().unwrap_or_else(|_| unreachable!())
|
|
&& stream.preserved));
|
|
assert!(status.active_user_streams.iter().any(|stream| stream == &other));
|
|
assert_eq!(status.active_users, 2);
|
|
assert_eq!(status.active_user_connections, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_preserved_non_adaptive_update_removes_active_stream_without_clearing_other_rows() {
|
|
let mut preserved = test_stream(1, "127.0.0.1:1234", Some("tok-vod"), PlaylistItemType::Video);
|
|
preserved.preserved = true;
|
|
let other = test_stream(2, "127.0.0.1:5678", Some("tok-other"), PlaylistItemType::LiveHls);
|
|
let mut status = shared::model::StatusCheck {
|
|
active_users: 2,
|
|
active_user_connections: 2,
|
|
active_user_streams: vec![
|
|
test_stream(1, "127.0.0.1:1234", Some("tok-vod"), PlaylistItemType::Video),
|
|
other.clone(),
|
|
],
|
|
..Default::default()
|
|
};
|
|
|
|
apply_active_user_change(&mut status, ActiveUserConnectionChange::Updated(preserved));
|
|
|
|
assert_eq!(status.active_user_streams, vec![other]);
|
|
assert_eq!(status.active_users, 2);
|
|
assert_eq!(status.active_user_connections, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_disconnected_stream_removes_only_matching_stream_identity() {
|
|
let removed = test_stream(1, "127.0.0.1:1234", Some("tok-live-a"), PlaylistItemType::Live);
|
|
let kept = test_stream(2, "127.0.0.1:1234", Some("tok-live-b"), PlaylistItemType::Live);
|
|
let mut status = shared::model::StatusCheck {
|
|
active_users: 1,
|
|
active_user_connections: 2,
|
|
active_user_streams: vec![removed.clone(), kept.clone()],
|
|
..Default::default()
|
|
};
|
|
|
|
apply_active_user_change(
|
|
&mut status,
|
|
ActiveUserConnectionChange::DisconnectedStream { addr: removed.addr, uid: removed.uid },
|
|
);
|
|
|
|
assert_eq!(status.active_user_streams, vec![kept]);
|
|
}
|
|
|
|
fn test_download(id: &str, status: TransferStatusDto, kind: TaskKindDto) -> FileDownloadDto {
|
|
FileDownloadDto {
|
|
id: id.to_string(),
|
|
title: format!("{id}.ts"),
|
|
kind,
|
|
priority: TaskPriorityDto::Background,
|
|
status,
|
|
retry_attempts: 0,
|
|
downloaded_bytes: 128,
|
|
total_bytes: Some(1024),
|
|
next_retry_at: None,
|
|
scheduled_start_at: None,
|
|
duration_secs: None,
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn downloads_snapshot_creates_running_pseudo_streams_only() {
|
|
let response = DownloadsResponse {
|
|
queue: Vec::new(),
|
|
finished: Vec::new(),
|
|
active: vec![
|
|
test_download("running", TransferStatusDto::Running, TaskKindDto::Download),
|
|
test_download("paused", TransferStatusDto::Paused, TaskKindDto::Recording),
|
|
],
|
|
};
|
|
let mut streams = Vec::new();
|
|
|
|
apply_downloads_snapshot(&mut streams, &response);
|
|
|
|
assert_eq!(streams.len(), 1);
|
|
assert_eq!(streams[0].provider.as_ref(), "Download Manager");
|
|
assert_eq!(streams[0].client_ip, "background-task");
|
|
assert_eq!(streams[0].channel.item_type, PlaylistItemType::Video);
|
|
}
|
|
|
|
#[test]
|
|
fn downloads_delta_updates_only_matching_pseudo_stream() {
|
|
let mut streams = Vec::new();
|
|
apply_downloads_snapshot(
|
|
&mut streams,
|
|
&DownloadsResponse {
|
|
queue: Vec::new(),
|
|
finished: Vec::new(),
|
|
active: vec![
|
|
test_download("one", TransferStatusDto::Running, TaskKindDto::Download),
|
|
test_download("two", TransferStatusDto::Running, TaskKindDto::Recording),
|
|
],
|
|
},
|
|
);
|
|
|
|
apply_downloads_delta(
|
|
&mut streams,
|
|
&DownloadsDelta::ActivePatched(test_download("one", TransferStatusDto::Paused, TaskKindDto::Download)),
|
|
);
|
|
|
|
assert_eq!(streams.len(), 1);
|
|
assert_eq!(streams[0].channel.title.as_ref(), "two.ts");
|
|
}
|
|
|
|
#[test]
|
|
fn downloads_delta_clears_pseudo_stream_when_active_stops_running() {
|
|
let mut streams = Vec::new();
|
|
apply_downloads_snapshot(
|
|
&mut streams,
|
|
&DownloadsResponse {
|
|
queue: Vec::new(),
|
|
finished: Vec::new(),
|
|
active: vec![test_download("running", TransferStatusDto::Running, TaskKindDto::Recording)],
|
|
},
|
|
);
|
|
|
|
apply_downloads_delta(
|
|
&mut streams,
|
|
&DownloadsDelta::ActivePatched(test_download(
|
|
"running",
|
|
TransferStatusDto::Completed,
|
|
TaskKindDto::Recording,
|
|
)),
|
|
);
|
|
|
|
assert!(streams.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn downloads_delta_preserves_pseudo_stream_start_timestamp() {
|
|
let mut streams = vec![download_task_to_stream_with_ts(
|
|
&test_download("running", TransferStatusDto::Running, TaskKindDto::Download),
|
|
123,
|
|
)];
|
|
|
|
apply_downloads_delta(
|
|
&mut streams,
|
|
&DownloadsDelta::ActivePatched(test_download("running", TransferStatusDto::Running, TaskKindDto::Download)),
|
|
);
|
|
|
|
assert_eq!(streams.len(), 1);
|
|
assert_eq!(streams[0].ts, 123);
|
|
}
|
|
|
|
#[test]
|
|
fn downloads_snapshot_preserves_pseudo_stream_start_timestamp() {
|
|
let mut streams = vec![download_task_to_stream_with_ts(
|
|
&test_download("running", TransferStatusDto::Running, TaskKindDto::Recording),
|
|
456,
|
|
)];
|
|
|
|
apply_downloads_snapshot(
|
|
&mut streams,
|
|
&DownloadsResponse {
|
|
queue: Vec::new(),
|
|
finished: Vec::new(),
|
|
active: vec![test_download("running", TransferStatusDto::Running, TaskKindDto::Recording)],
|
|
},
|
|
);
|
|
|
|
assert_eq!(streams.len(), 1);
|
|
assert_eq!(streams[0].ts, 456);
|
|
}
|
|
}
|