mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-15 20:32:27 +02:00
Added copy to clipboard menu for stream table
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user