reverse proxy mode for kodi export

This commit is contained in:
euzu
2024-12-30 12:40:22 +01:00
parent a027e808b1
commit 73615701b5
9 changed files with 102 additions and 34 deletions
+1
View File
@@ -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.
+2 -9
View File
@@ -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,
+2 -2
View File
@@ -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)
}
+11
View File
@@ -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
View File
@@ -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.
+45 -10
View File
@@ -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 -2
View File
@@ -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(),
+1 -1
View File
@@ -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 {
+5 -4
View File
@@ -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}");
}