Added copy to clipboard menu for stream table

This commit is contained in:
euzu
2025-11-24 14:29:05 +01:00
parent 92bcb63628
commit 0119abe8bb
8 changed files with 92 additions and 26 deletions
+4 -2
View File
@@ -166,10 +166,11 @@ pub(in crate::api) async fn handle_hls_stream_request(
async fn get_stream_channel(app_state: &Arc<AppState>, target: &Arc<ConfigTarget>, virtual_id: u32) -> Option<StreamChannel> {
if target.has_output(TargetType::Xtream) {
if let Ok((pli, _)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, app_state, target, None).await {
return Some(pli.to_stream_channel());
return Some(pli.to_stream_channel(target.id));
}
}
m3u_get_item_for_stream_id(virtual_id, app_state, target).await.ok().map(|pli| pli.to_stream_channel())
let target_id = target.id;
m3u_get_item_for_stream_id(virtual_id, app_state, target).await.ok().map(|pli| pli.to_stream_channel(target_id))
}
async fn resolve_stream_channel(
@@ -181,6 +182,7 @@ async fn resolve_stream_channel(
let mut channel = match get_stream_channel(app_state, target, virtual_id).await {
Some(channel) => channel,
None => StreamChannel {
target_id: target.id,
virtual_id,
provider_id: 0,
item_type: PlaylistItemType::LiveHls,
+2 -2
View File
@@ -154,7 +154,7 @@ async fn m3u_api_stream(
fingerprint,
app_state,
session,
pli.to_stream_channel(),
pli.to_stream_channel(target.id),
req_headers,
&input,
&user,
@@ -224,7 +224,7 @@ async fn m3u_api_stream(
fingerprint,
app_state,
&session_key,
pli.to_stream_channel(),
pli.to_stream_channel(target.id),
session_url,
req_headers,
&input,
+3 -3
View File
@@ -291,7 +291,7 @@ async fn xtream_player_api_stream(
.into_response();
}
let stream_channel = create_stream_channel_with_type(&pli, item_type);
let stream_channel = create_stream_channel_with_type(target.id, &pli, item_type);
if session.virtual_id == virtual_id && is_seek_request(cluster, req_headers).await {
// partial request means we are in reverse proxy mode, seek happened
@@ -380,7 +380,7 @@ async fn xtream_player_api_stream(
.into_response();
}
let stream_channel = create_stream_channel_with_type(&pli, item_type);
let stream_channel = create_stream_channel_with_type(target.id, &pli, item_type);
stream_response(
fingerprint,
@@ -516,7 +516,7 @@ async fn xtream_player_api_stream_with_token(
fingerprint,
app_state,
session_key.as_str(),
pli.to_stream_channel(),
pli.to_stream_channel(target.id),
&stream_url,
req_headers,
&input,
+3 -3
View File
@@ -261,8 +261,8 @@ fn check_alias_user_state(cfg: &Arc<Config>, client: &Arc<reqwest::Client>, inpu
tokio::spawn(async move {
if let Some(aliases) = input.aliases.as_ref() {
for alias in aliases {
// Random wait time 5–20 seconds to avoid provider block
let delay = u64::from(fastrand::u32(5..=20));
// Random wait time 60–180 seconds to avoid provider block
let delay = u64::from(fastrand::u32(60..=180));
tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await;
if let (Some(username), Some(password)) =
@@ -315,4 +315,4 @@ pub fn create_vod_info_from_item(target: &ConfigTarget, user: &ProxyUserCredenti
"stream_id": {stream_id}
}}
}}"#)
}
}
@@ -14,7 +14,9 @@ use std::rc::Rc;
use std::str::FromStr;
use wasm_bindgen::JsCast;
use web_sys::Element;
use yew::platform::spawn_local;
use yew::prelude::*;
use yew_hooks::use_clipboard;
use yew_i18n::use_translation;
const LIVE: &str = "Live";
@@ -24,6 +26,11 @@ const CATCHUP: &str = "Archive";
const HLS: &str = "HLS";
const DASH: &str = "DASH";
const KICK: &str = "kick";
const COPY_LINK_TULIPROX_VIRTUAL_ID: &str = "copy_link_tuliprox_virtual_id";
const COPY_LINK_TULIPROX_WEBPLAYER_URL: &str = "copy_link_tuliprox_webplayer_url";
const COPY_LINK_PROVIDER_URL: &str = "copy_link_provider_url";
const HEADERS: [&str; 12] = [
"EMPTY",
"USERNAME",
@@ -70,7 +77,8 @@ pub struct StreamsTableProps {
#[function_component]
pub fn StreamsTable(props: &StreamsTableProps) -> Html {
let translate = use_translation();
let services = use_service_context();
let service_ctx = use_service_context();
let clipboard = use_clipboard();
let config_ctx = use_context::<ConfigContext>().expect("Config context not found");
let popup_anchor_ref = use_state(|| None::<web_sys::Element>);
let popup_is_open = use_state(|| false);
@@ -214,22 +222,59 @@ pub fn StreamsTable(props: &StreamsTableProps) -> Html {
})
};
let copy_to_clipboard: Callback<String> = {
let clipboard = clipboard.clone();
let services = service_ctx.clone();
let translate = translate.clone();
Callback::from(move |text: String| {
if *clipboard.is_supported {
clipboard.write_text(text);
} else {
services.toastr.error(translate.t("MESSAGES.CLIPBOARD_NOT_SUPPORTED"));
}
})
};
let handle_menu_click = {
let popup_is_open_state = popup_is_open.clone();
let translate = translate.clone();
let services_ctx = services.clone();
let services = service_ctx.clone();
let selected_dto = selected_dto.clone();
let copy_to_clipboard = copy_to_clipboard.clone();
Callback::from(move |(name, _): (String, _)| {
if let Ok(action) = StreamsTableAction::from_str(&name) {
match action {
StreamsTableAction::Kick => {
if let Some(dto) = (*selected_dto).as_ref() {
if !services_ctx.websocket.send_message(ProtocolMessage::UserAction(UserCommand::Kick(dto.addr))) {
services_ctx.toastr.error(translate.t("MESSAGES.FAILED_TO_KICK_USER_STREAM"));
if !services.websocket.send_message(ProtocolMessage::UserAction(UserCommand::Kick(dto.addr))) {
services.toastr.error(translate.t("MESSAGES.FAILED_TO_KICK_USER_STREAM"));
}
}
}
StreamsTableAction::CopyLinkTuliproxVirtualId => {
if let Some(dto) = &*selected_dto {
copy_to_clipboard.emit(dto.channel.virtual_id.to_string());
}
}
StreamsTableAction::CopyLinkProviderUrl => {
if let Some(dto) = &*selected_dto {
copy_to_clipboard.emit(dto.channel.url.clone());
}
}
StreamsTableAction::CopyLinkTuliproxWebPlayerUrl => {
if let Some(dto) = &*selected_dto {
let target_id = dto.channel.target_id;
let virtual_id = dto.channel.virtual_id;
let cluster = dto.channel.cluster;
let services = services.clone();
let copy_to_clipboard = copy_to_clipboard.clone();
spawn_local(async move {
if let Some(url) = services.playlist.get_playlist_webplayer_url(target_id, virtual_id, cluster).await {
copy_to_clipboard.emit(url);
}
});
}
}
}
}
popup_is_open_state.set(false);
@@ -245,6 +290,9 @@ pub fn StreamsTable(props: &StreamsTableProps) -> Html {
<Table::<StreamInfo> definition={definition.clone()} />
<PopupMenu is_open={*popup_is_open} anchor_ref={(*popup_anchor_ref).clone()} on_close={handle_popup_close}>
<MenuItem icon="Disconnect" name={StreamsTableAction::Kick.to_string()} label={translate.t("LABEL.KICK")} onclick={&handle_menu_click} class="tp__delete_action"></MenuItem>
<MenuItem icon="Clipboard" name={StreamsTableAction::CopyLinkTuliproxVirtualId.to_string()} label={translate.t("LABEL.COPY_LINK_TULIPROX_VIRTUAL_ID")} onclick={&handle_menu_click}></MenuItem>
<MenuItem icon="Clipboard" name={StreamsTableAction::CopyLinkTuliproxWebPlayerUrl.to_string()} label={translate.t("LABEL.COPY_LINK_TULIPROX_WEBPLAYER_URL")} onclick={&handle_menu_click}></MenuItem>
<MenuItem icon="Clipboard" name={StreamsTableAction::CopyLinkProviderUrl.to_string()} label={translate.t("LABEL.COPY_LINK_PROVIDER_URL")} onclick={&handle_menu_click}></MenuItem>
</PopupMenu>
</>
}
@@ -259,12 +307,18 @@ pub fn StreamsTable(props: &StreamsTableProps) -> Html {
#[derive(Debug, Clone, Eq, PartialEq)]
enum StreamsTableAction {
Kick,
CopyLinkTuliproxVirtualId,
CopyLinkTuliproxWebPlayerUrl,
CopyLinkProviderUrl,
}
impl Display for StreamsTableAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Self::Kick => "kick",
Self::Kick => KICK,
Self::CopyLinkTuliproxVirtualId => COPY_LINK_TULIPROX_VIRTUAL_ID,
Self::CopyLinkTuliproxWebPlayerUrl => COPY_LINK_TULIPROX_WEBPLAYER_URL,
Self::CopyLinkProviderUrl => COPY_LINK_PROVIDER_URL,
})
}
}
@@ -273,8 +327,14 @@ impl FromStr for StreamsTableAction {
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, TuliproxError> {
if s.eq("kick") {
if s.eq(KICK) {
Ok(Self::Kick)
} else if s.eq(COPY_LINK_TULIPROX_VIRTUAL_ID) {
Ok(Self::CopyLinkTuliproxVirtualId)
} else if s.eq(COPY_LINK_TULIPROX_WEBPLAYER_URL) {
Ok(Self::CopyLinkTuliproxWebPlayerUrl)
} else if s.eq(COPY_LINK_PROVIDER_URL) {
Ok(Self::CopyLinkProviderUrl)
} else {
create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown Stream Action: {}", s)
}
@@ -144,11 +144,12 @@ pub fn PlaylistExplorer() -> Html {
if let Some(dto) = &*selected_channel {
let copy_to_clipboard = copy_to_clipboard.clone();
let services = services.clone();
let dto = dto.clone();
let virtual_id = dto.virtual_id;
let cluster = dto.xtream_cluster.unwrap_or_default();
let translate_clone = translate_clone.clone();
let target_id = *target_id;
spawn_local(async move {
if let Some(url) = services.playlist.get_playlist_webplayer_url(target_id, &dto).await {
if let Some(url) = services.playlist.get_playlist_webplayer_url(target_id, virtual_id, cluster).await {
copy_to_clipboard.emit(url);
services.toastr.success(translate_clone.t("MESSAGES.PLAYLIST.WEBPLAYER_URL_COPY_TO_CLIPBOARD"));
}
+4 -4
View File
@@ -1,6 +1,6 @@
use crate::services::{get_base_href, request_post, ACCEPT_PREFER_BIN};
use log::error;
use shared::model::{CommonPlaylistItem, EpgTv, PlaylistCategoriesResponse, PlaylistEpgRequest, PlaylistRequest, UiPlaylistCategories, WebplayerUrlRequest};
use shared::model::{EpgTv, PlaylistCategoriesResponse, PlaylistEpgRequest, PlaylistRequest, UiPlaylistCategories, WebplayerUrlRequest, XtreamCluster};
use std::rc::Rc;
use shared::utils::{concat_path_leading_slash};
@@ -41,11 +41,11 @@ impl PlaylistService {
})
}
pub async fn get_playlist_webplayer_url(&self, target_id: u16, dto: &Rc<CommonPlaylistItem>) -> Option<String> {
pub async fn get_playlist_webplayer_url(&self, target_id: u16, virtual_id: u32, cluster: XtreamCluster) -> Option<String> {
let request = WebplayerUrlRequest {
target_id,
virtual_id: dto.virtual_id,
cluster: dto.xtream_cluster.unwrap_or_default(),
virtual_id,
cluster,
};
request_post::<&WebplayerUrlRequest, String>(&self.playlist_api_webplayer_url_path, &request, None, Some("text/plain".to_string())).await.unwrap_or_else(|err| {
error!("{err}");
+7 -4
View File
@@ -5,6 +5,7 @@ use crate::utils::{current_time_secs, longest};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StreamChannel {
pub target_id: u16,
pub virtual_id: u32,
pub provider_id: u32,
pub item_type: PlaylistItemType,
@@ -15,15 +16,16 @@ pub struct StreamChannel {
pub shared: bool,
}
pub fn create_stream_channel_with_type(pli: &XtreamPlaylistItem, item_type: PlaylistItemType) -> StreamChannel {
let mut stream_channel = pli.to_stream_channel();
pub fn create_stream_channel_with_type(target_id: u16, pli: &XtreamPlaylistItem, item_type: PlaylistItemType) -> StreamChannel {
let mut stream_channel = pli.to_stream_channel(target_id);
stream_channel.item_type = item_type;
stream_channel
}
impl XtreamPlaylistItem {
pub fn to_stream_channel(&self) -> StreamChannel {
pub fn to_stream_channel(&self, target_id: u16) -> StreamChannel {
StreamChannel {
target_id,
virtual_id: self.virtual_id,
provider_id: self.provider_id,
item_type: self.item_type,
@@ -37,8 +39,9 @@ impl XtreamPlaylistItem {
}
impl M3uPlaylistItem {
pub fn to_stream_channel(&self) -> StreamChannel {
pub fn to_stream_channel(&self, target_id: u16) -> StreamChannel {
StreamChannel {
target_id,
virtual_id: self.virtual_id,
provider_id: self.get_provider_id().unwrap_or_default(),
item_type: self.item_type,