mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-11 10:22:10 +02:00
reverse proxy mode for kodi export
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
- added live channel connection sharing in reverse proxy mode. To activate set `share_live_streams` in target options.
|
||||
- added `info` and `tmdb-id` caching for vod.
|
||||
- The `kodi` format for movies can contain the `tmdb-id` (_optional_). To add the `tmdb-id` you can set now `kodi_style`, `xtream_resolve_vod`, `xtream_resolve_vod_delay`, `xtream_resolve_series` and `xtream_resolve_series_delay` to target options.
|
||||
- `kodi` output can now have `username` attribute to use reverse proxy mode when combined with `xtream` output.
|
||||
|
||||
# 2.0.10 (2024-12-03)
|
||||
- added Target Output Option `m3u_include_type_in_url`, default false. This adds `live`, `movie`, `series` to the url of the stream in reverse proxy mode.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
use crate::api::model::shared_stream::SharedStream;
|
||||
use crate::model::api_proxy::{ApiProxyServerInfo, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, ConfigInput, ConfigTarget};
|
||||
use crate::model::api_proxy::{ProxyUserCredentials};
|
||||
use crate::model::config::{ConfigInput, ConfigTarget};
|
||||
use crate::utils::request_utils;
|
||||
use crate::utils::request_utils::mask_sensitive_info;
|
||||
use actix_web::http::header::{HeaderValue, CACHE_CONTROL};
|
||||
@@ -56,13 +56,6 @@ pub fn get_user_target<'a>(api_req: &'a UserApiRequest, app_state: &'a AppState)
|
||||
get_user_target_by_credentials(username, password, api_req, app_state)
|
||||
}
|
||||
|
||||
pub fn get_user_server_info(cfg: &Config, user: &ProxyUserCredentials) -> ApiProxyServerInfo {
|
||||
let server_info_list = cfg.t_api_proxy.read().unwrap().as_ref().unwrap().server.clone();
|
||||
let server_info_name = user.server.as_ref().map_or("default", |server_name| server_name.as_str());
|
||||
server_info_list.iter().find(|c| c.name.eq(server_info_name)).map_or_else(|| server_info_list.first().unwrap().clone(), std::clone::Clone::clone)
|
||||
}
|
||||
|
||||
|
||||
/// Creates a notify stream for the given URL if a shared stream exists.
|
||||
async fn create_notify_stream(
|
||||
app_state: &AppState,
|
||||
|
||||
@@ -14,7 +14,7 @@ use log::{debug, error, warn, Level};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::api::api_utils::{get_user_server_info, get_user_target, get_user_target_by_credentials, is_stream_share_enabled, serve_file, stream_response};
|
||||
use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, is_stream_share_enabled, serve_file, stream_response};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
use crate::api::model::xtream::XtreamAuthorizationResponse;
|
||||
@@ -158,7 +158,7 @@ fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &str, action_p
|
||||
}
|
||||
|
||||
fn get_user_info(user: &ProxyUserCredentials, cfg: &Config) -> XtreamAuthorizationResponse {
|
||||
let server_info = get_user_server_info(cfg, user);
|
||||
let server_info = cfg.get_user_server_info(user);
|
||||
XtreamAuthorizationResponse::new(&server_info, user)
|
||||
}
|
||||
|
||||
|
||||
@@ -297,4 +297,15 @@ impl ApiProxyConfig {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_user_credentials(&self,username: &str) -> Option<ProxyUserCredentials> {
|
||||
let result = self.user.iter()
|
||||
.flat_map(|target_user| &target_user.credentials)
|
||||
.find(|credential| credential.username == username)
|
||||
.cloned();
|
||||
if result.is_none() {
|
||||
debug!("Could not find any user {username}");
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
+34
-6
@@ -18,7 +18,7 @@ use crate::filter::{get_filter, prepare_templates, Filter, MockValueProcessor, P
|
||||
use crate::info_err;
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::messaging::MsgKind;
|
||||
use crate::model::api_proxy::{ApiProxyConfig, ProxyUserCredentials};
|
||||
use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials};
|
||||
use crate::model::mapping::Mapping;
|
||||
use crate::model::mapping::Mappings;
|
||||
use crate::utils::default_utils::{default_as_default, default_as_true, default_as_two_u16};
|
||||
@@ -318,11 +318,14 @@ pub struct ConfigTargetOptions {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TargetOutput {
|
||||
#[serde(alias = "type")]
|
||||
pub target: TargetType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filename: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
|
||||
@@ -366,26 +369,37 @@ impl ConfigTarget {
|
||||
let mut m3u_cnt = 0;
|
||||
let mut strm_cnt = 0;
|
||||
let mut xtream_cnt = 0;
|
||||
let mut strm_needs_xtream = false;
|
||||
for format in &self.output {
|
||||
let has_username = if let Some(username) = &format.username { !username.trim().is_empty() } else { false };
|
||||
let has_filename = if let Some(fname) = &format.filename { !fname.trim().is_empty() } else { false };
|
||||
|
||||
match format.target {
|
||||
TargetType::M3u => {
|
||||
m3u_cnt += 1;
|
||||
if has_username {
|
||||
warn!("Username for target output m3u is ignored: {}", self.name);
|
||||
}
|
||||
}
|
||||
TargetType::Strm => {
|
||||
strm_cnt += 1;
|
||||
if format.filename.is_none() {
|
||||
if !has_filename {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "filename is required for strm type: {}", self.name);
|
||||
}
|
||||
if has_username {
|
||||
strm_needs_xtream = true;
|
||||
}
|
||||
}
|
||||
TargetType::Xtream => {
|
||||
xtream_cnt += 1;
|
||||
if default_as_default().eq_ignore_ascii_case(&self.name) {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "unique target name is required for xtream type: {}", self.name);
|
||||
}
|
||||
if let Some(fname) = &format.filename {
|
||||
if !fname.trim().is_empty() {
|
||||
warn!("Filename for target output xtream is ignored: {}", self.name);
|
||||
}
|
||||
if has_username {
|
||||
warn!("Username for target output xtream is ignored: {}", self.name);
|
||||
}
|
||||
if has_filename {
|
||||
warn!("Filename for target output xtream is ignored: {}", self.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,6 +409,10 @@ impl ConfigTarget {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Multiple output formats with same type : {}", self.name);
|
||||
}
|
||||
|
||||
if strm_cnt > 0 && strm_needs_xtream && xtream_cnt == 0 {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "strm output with a username is only permitted when used in combination with xtream output: {}", self.name);
|
||||
}
|
||||
|
||||
if let Some(watch) = &self.watch {
|
||||
let regexps: Result<Vec<regex::Regex>, _> = watch.iter().map(|s| regex::Regex::new(s)).collect();
|
||||
match regexps {
|
||||
@@ -903,6 +921,10 @@ impl Config {
|
||||
self.t_api_proxy.read().unwrap().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name_by_token(token)))
|
||||
}
|
||||
|
||||
pub fn get_user_credentials(&self, username: &str) -> Option<ProxyUserCredentials> {
|
||||
self.t_api_proxy.read().unwrap().as_ref().and_then(|api_proxy| api_proxy.get_user_credentials(username))
|
||||
}
|
||||
|
||||
pub fn get_input_by_id(&self, input_id: u16) -> Option<&ConfigInput> {
|
||||
for source in &self.sources {
|
||||
for input in &source.inputs {
|
||||
@@ -1041,6 +1063,12 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user_server_info(&self, user: &ProxyUserCredentials) -> ApiProxyServerInfo {
|
||||
let server_info_list = self.t_api_proxy.read().unwrap().as_ref().unwrap().server.clone();
|
||||
let server_info_name = user.server.as_ref().map_or("default", |server_name| server_name.as_str());
|
||||
server_info_list.iter().find(|c| c.name.eq(server_info_name)).map_or_else(|| server_info_list.first().unwrap().clone(), std::clone::Clone::clone)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the targets that were specified as parameters.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::model::config::{Config, ConfigTarget};
|
||||
use crate::model::api_proxy::{ApiProxyServerInfo, ProxyType, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, ConfigTarget, TargetOutput};
|
||||
use crate::model::playlist::{FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemType};
|
||||
use crate::model::xtream::XtreamSeriesEpisode;
|
||||
use crate::repository::bplustree::BPlusTree;
|
||||
@@ -11,13 +12,13 @@ use crate::{create_m3u_filter_error_result, notify_err};
|
||||
use chrono::Datelike;
|
||||
use log::error;
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::LazyLock;
|
||||
use serde::Serialize;
|
||||
|
||||
struct KodiStyle {
|
||||
year: Regex,
|
||||
@@ -72,7 +73,6 @@ fn kodi_style_rename_year<'a>(
|
||||
style: &KodiStyle,
|
||||
release_date: Option<&'a String>,
|
||||
) -> (&'a str, Option<u32>) {
|
||||
|
||||
let mut years = Vec::new();
|
||||
|
||||
let cur_year = u32::try_from(chrono::Utc::now().year()).unwrap_or(0);
|
||||
@@ -136,7 +136,7 @@ async fn kodi_style_rename(cfg: &Config, strm_item_info: &StrmItemInfo, style: &
|
||||
} {
|
||||
match value {
|
||||
InputTmdbIndexValue::Video(vod_record) => vod_record.tmdb_id,
|
||||
InputTmdbIndexValue::Series(episode) => episode.tmdb_id,
|
||||
InputTmdbIndexValue::Series(episode) => episode.tmdb_id,
|
||||
}
|
||||
} else { 0 };
|
||||
|
||||
@@ -264,6 +264,7 @@ struct StrmItemInfo {
|
||||
title: Rc<String>,
|
||||
item_type: PlaylistItemType,
|
||||
provider_id: Option<u32>,
|
||||
virtual_id: u32,
|
||||
input_id: u16,
|
||||
url: Rc<String>,
|
||||
series_name: Option<String>,
|
||||
@@ -278,6 +279,7 @@ fn extract_item_info(pli: &PlaylistItem) -> StrmItemInfo {
|
||||
let title = Rc::clone(&header.title);
|
||||
let item_type = header.item_type;
|
||||
let provider_id = header.get_provider_id();
|
||||
let virtual_id = header.virtual_id;
|
||||
let input_id = header.input_id;
|
||||
// TODO reverse proxy url
|
||||
let url = Rc::clone(&header.url);
|
||||
@@ -292,7 +294,7 @@ fn extract_item_info(pli: &PlaylistItem) -> StrmItemInfo {
|
||||
(series_name, release_date, season, episode)
|
||||
} else { (None, None, None, None) };
|
||||
|
||||
StrmItemInfo { group, title, item_type, provider_id, input_id, url, series_name, release_date, season, episode }
|
||||
StrmItemInfo { group, title, item_type, provider_id, virtual_id, input_id, url, series_name, release_date, season, episode }
|
||||
}
|
||||
|
||||
fn prepare_strm_output_directory(cleanup: bool, path: &PathBuf) -> Result<(), M3uFilterError> {
|
||||
@@ -319,15 +321,23 @@ fn get_strm_output_options(target: &ConfigTarget) -> (bool, bool, bool) {
|
||||
|o| (o.underscore_whitespace, o.cleanup, o.kodi_style))
|
||||
}
|
||||
|
||||
pub async fn kodi_write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &[PlaylistGroup], filename: Option<&str>) -> Result<(), M3uFilterError> {
|
||||
pub async fn kodi_write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &[PlaylistGroup], output: &TargetOutput) -> Result<(), M3uFilterError> {
|
||||
let mut result = Ok(());
|
||||
if !new_playlist.is_empty() {
|
||||
if filename.is_none() {
|
||||
if output.filename.is_none() {
|
||||
return Err(notify_err!("write strm playlist failed: ".to_string()));
|
||||
}
|
||||
let credentials_and_server_info = output.username.as_ref()
|
||||
.and_then(|username| cfg.get_user_credentials(username))
|
||||
.filter(|credentials| credentials.proxy == ProxyType::Reverse)
|
||||
.map(|credentials| {
|
||||
let server_info = cfg.get_user_server_info(&credentials);
|
||||
(credentials, server_info)
|
||||
});
|
||||
|
||||
let (underscore_whitespace, cleanup, kodi_style) = get_strm_output_options(target);
|
||||
let Some(path) = file_utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&filename.as_ref().unwrap()))) else {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Failed to get file path for {}", filename.unwrap_or(""));
|
||||
let Some(path) = file_utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&output.filename.as_ref().unwrap()))) else {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Failed to get file path for {}", output.filename.as_deref().unwrap_or(""));
|
||||
};
|
||||
prepare_strm_output_directory(cleanup, &path)?;
|
||||
let mut input_tmdb_indexes: InputTmdbIndexMap = HashMap::new();
|
||||
@@ -353,10 +363,13 @@ pub async fn kodi_write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_p
|
||||
error!("cant create directory: {output_path:?}");
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "failed to create directory for strm playlist:{output_path:?} {e}");
|
||||
};
|
||||
|
||||
let url = get_strm_url(credentials_and_server_info.as_ref(), &str_item_info);
|
||||
|
||||
let file_path = output_path.join(format!("{strm_file_name}.strm"));
|
||||
match File::create(&file_path) {
|
||||
Ok(mut strm_file) => {
|
||||
match file_utils::check_write(&strm_file.write_all(str_item_info.url.as_bytes())) {
|
||||
match file_utils::check_write(&strm_file.write_all(url.as_bytes())) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
error!("failed to write strm playlist: {err}");
|
||||
@@ -374,3 +387,25 @@ pub async fn kodi_write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_p
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn get_strm_url(credentials_and_server_info: Option<&(ProxyUserCredentials, ApiProxyServerInfo)>, str_item_info: &StrmItemInfo) -> String {
|
||||
let url = credentials_and_server_info.as_ref()
|
||||
.map_or_else(|| str_item_info.url.to_string(),
|
||||
|(user, server_info)|
|
||||
if let Some(stream_type) = match str_item_info.item_type {
|
||||
PlaylistItemType::Series => Some("series"),
|
||||
PlaylistItemType::Live => Some("live"),
|
||||
PlaylistItemType::Video => Some("movie"),
|
||||
_ => None,
|
||||
} {
|
||||
format!("{}/{stream_type}/{}/{}/{}",
|
||||
server_info.get_base_url(),
|
||||
user.username,
|
||||
user.password,
|
||||
str_item_info.virtual_id)
|
||||
} else {
|
||||
str_item_info.url.to_string()
|
||||
},
|
||||
);
|
||||
url
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::api::api_utils::get_user_server_info;
|
||||
use crate::info_err;
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::model::api_proxy::{ProxyType, ProxyUserCredentials};
|
||||
@@ -44,7 +43,7 @@ impl M3uPlaylistIterator {
|
||||
let include_type_in_url = target_options.is_some_and( |opts| opts.m3u_include_type_in_url);
|
||||
let mask_redirect_url = target_options.is_some_and(|opts| opts.m3u_mask_redirect_url);
|
||||
|
||||
let server_info = get_user_server_info(cfg, user);
|
||||
let server_info = cfg.get_user_server_info(user);
|
||||
Ok(Self {
|
||||
reader,
|
||||
base_url: server_info.get_base_url(),
|
||||
|
||||
@@ -49,7 +49,7 @@ pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>,
|
||||
let result = match output.target {
|
||||
TargetType::M3u => m3u_write_playlist(target, cfg, &target_path, playlist).await,
|
||||
TargetType::Xtream => xtream_write_playlist(target, cfg, playlist).await,
|
||||
TargetType::Strm => kodi_write_strm_playlist(target, cfg, playlist, output.filename.as_deref()).await,
|
||||
TargetType::Strm => kodi_write_strm_playlist(target, cfg, playlist, output).await,
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
|
||||
@@ -12,13 +12,13 @@ use crate::model::config::{Config, ConfigInput, ConfigTarget};
|
||||
use crate::model::playlist::{PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster, XtreamPlaylistItem};
|
||||
use crate::model::xtream::{XtreamMappingOptions, XtreamSeriesEpisode};
|
||||
use crate::repository::bplustree::{BPlusTree, BPlusTreeQuery, BPlusTreeUpdate};
|
||||
use crate::repository::indexed_document::{IndexedDocumentGarbageCollector, IndexedDocumentWriter, IndexedDocumentDirectAccess};
|
||||
use crate::repository::indexed_document::{IndexedDocumentDirectAccess, IndexedDocumentGarbageCollector, IndexedDocumentWriter};
|
||||
use crate::repository::storage::{get_input_storage_path, get_target_id_mapping_file, get_target_storage_path, hash_string, FILE_SUFFIX_DB, FILE_SUFFIX_INDEX};
|
||||
use crate::repository::target_id_mapping::{TargetIdMapping, VirtualIdRecord};
|
||||
use crate::repository::xtream_playlist_iterator::XtreamPlaylistIterator;
|
||||
use crate::utils::json_utils::{json_iter_array, json_write_documents_to_file};
|
||||
use crate::{create_m3u_filter_error, create_m3u_filter_error_result, notify_err, info_err};
|
||||
use crate::utils::file_utils::open_readonly_file;
|
||||
use crate::utils::json_utils::{json_iter_array, json_write_documents_to_file};
|
||||
use crate::{create_m3u_filter_error, create_m3u_filter_error_result, info_err, notify_err};
|
||||
|
||||
pub static COL_CAT_LIVE: &str = "cat_live";
|
||||
pub static COL_CAT_SERIES: &str = "cat_series";
|
||||
@@ -391,6 +391,7 @@ pub async fn xtream_get_item_for_stream_id(
|
||||
xtream_read_series_item_for_stream_id(config, virtual_id, &storage_path).await
|
||||
}
|
||||
PlaylistItemType::Series => {
|
||||
// TODO reverse proxy mode not working when resolve_series
|
||||
let mut item = xtream_read_series_item_for_stream_id(config, mapping.parent_virtual_id, &storage_path).await?;
|
||||
item.provider_id = mapping.provider_id;
|
||||
Ok(item)
|
||||
@@ -829,7 +830,7 @@ pub async fn xtream_update_input_series_episodes_record_from_wal_file(
|
||||
match bincode::deserialize(&buffer[0..len]) {
|
||||
Ok(episode) => {
|
||||
tree_record_index.insert(provider_id, episode);
|
||||
},
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to delete deserialize record WAL file for series episode {err}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user