user boquet for m3u playlist

This commit is contained in:
euzu
2025-03-08 05:43:13 +01:00
parent d41c59288e
commit fcc82dec74
24 changed files with 649 additions and 362 deletions
+72 -45
View File
@@ -1,41 +1,47 @@
use std::sync::Arc;
use crate::api::api_utils::{get_user_target_by_username, get_username_from_auth_header};
use crate::api::model::app_state::AppState;
use crate::auth::authenticator::validator_user;
use crate::model::config::TargetType;
use crate::model::playlist_categories::PlaylistCategoriesDto;
use crate::model::config::{Config, ConfigTarget, TargetType};
use crate::model::playlist::XtreamCluster;
use crate::model::playlist_categories::PlaylistBouquetDto;
use crate::model::xtream::PlaylistXtreamCategory;
use crate::repository::user_repository::{load_user_bouquet_as_json, save_user_bouquet};
use crate::repository::xtream_repository;
use crate::repository::{m3u_repository};
use actix_web::body::BodyStream;
use actix_web::middleware::Compress;
use actix_web::{web, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use actix_web_httpauth::middleware::HttpAuthentication;
use bytes::Bytes;
use futures::stream;
use serde::{Deserialize, Serialize};
use std::io::Error;
use std::path::PathBuf;
use futures::{stream, StreamExt};
use log::error;
use std::collections::HashSet;
use std::sync::Arc;
use crate::repository::xtream_repository::xtream_get_playlist_categories;
#[derive(Deserialize, Serialize)]
struct PlaylistXtreamCategory {
#[serde(alias = "category_id")]
pub id: String,
#[serde(alias = "category_name")]
pub name: String,
fn get_categories_from_xtream(categories: Option<Vec<PlaylistXtreamCategory>>) -> Vec<String> {
let mut groups: Vec<String> = Vec::new();
if let Some(cats) = categories {
for category in cats {
groups.push(category.name.to_string());
}
}
groups
}
pub(crate) async fn get_categories_content(action: Result<(Option<PathBuf>, Option<String>), Error>) -> Option<String> {
if let Ok((Some(file_path), _content)) = action {
if let Ok(content) = tokio::fs::read_to_string(&file_path).await {
// TODO deserialize like sax parser
if let Ok(categories) = serde_json::from_str::<Vec<PlaylistXtreamCategory>>(&content) {
return serde_json::to_string(&categories).ok();
fn get_categories_from_m3u_playlist(target: &ConfigTarget, config: &Arc<Config>) -> Vec<String> {
let mut groups = Vec::new();
if let Some((_guard, iter)) = m3u_repository::iter_raw_m3u_playlist(config, target) {
let mut unique_groups = HashSet::new();
for (item, _has_next) in iter {
if !unique_groups.contains(item.group.as_str()) {
unique_groups.insert(item.group.to_string());
groups.push(item.group.to_string());
}
}
}
None
groups
}
async fn playlist_categories(
@@ -49,24 +55,44 @@ async fn playlist_categories(
}
let config = &app_state.config;
let target_name = &target.name;
if target.has_output(&TargetType::Xtream) {
let live_categories = get_categories_content(xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_LIVE)).await;
let vod_categories = get_categories_content(xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_VOD)).await;
let series_categories = get_categories_content(xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_SERIES)).await;
let json_stream =
stream::iter(vec![
Ok::<Bytes, String>(Bytes::from(r#"{"live": "#.to_string())),
Ok::<Bytes, String>(Bytes::from(live_categories.unwrap_or("null".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#", "vod": "#.to_string())),
Ok::<Bytes, String>(Bytes::from(vod_categories.unwrap_or("null".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#", "series": "#.to_string())),
Ok::<Bytes, String>(Bytes::from(series_categories.unwrap_or("null".to_string()))),
Ok::<Bytes, String>(Bytes::from(r"}".to_string())),
]);
return HttpResponse::Ok()
.content_type(mime::APPLICATION_JSON)
.body(BodyStream::new(json_stream));
} else if target.has_output(&TargetType::M3u) {}
let xtream_stream = if target.has_output(&TargetType::Xtream) {
let live_categories = get_categories_from_xtream(xtream_get_playlist_categories(config, target_name, XtreamCluster::Live).await);
let vod_categories = get_categories_from_xtream(xtream_get_playlist_categories(config, target_name, XtreamCluster::Video).await);
let series_categories = get_categories_from_xtream(xtream_get_playlist_categories(config, target_name, XtreamCluster::Series).await);
stream::iter(vec![
Ok::<Bytes, String>(Bytes::from(r#"{"live": "#)),
Ok::<Bytes, String>(Bytes::from(serde_json::to_string(&live_categories).unwrap_or("[]".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#", "vod": "#.to_string())),
Ok::<Bytes, String>(Bytes::from(serde_json::to_string(&vod_categories).unwrap_or("[]".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#", "series": "#)),
Ok::<Bytes, String>(Bytes::from(serde_json::to_string(&series_categories).unwrap_or("[]".to_string()))),
Ok::<Bytes, String>(Bytes::from(r"}")),
])
} else {
stream::iter(vec![Ok::<Bytes, String>(Bytes::from(r#"{"live":[],"vod":[],"series":[]}"#))])
};
let m3u_stream = if target.has_output(&TargetType::M3u) {
let live_categories = get_categories_from_m3u_playlist(target, config);
stream::iter(vec![
Ok::<Bytes, String>(Bytes::from(r#"{"live": "#)),
Ok::<Bytes, String>(Bytes::from(serde_json::to_string(&live_categories).unwrap_or("[]".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#","vod":[],"series":[]}"#)),
])
} else {
stream::iter(vec![Ok::<Bytes, String>(Bytes::from(r#"{"live":[],"vod":[],"series":[]}"#))])
};
let json_stream = stream::once(async { Ok::<Bytes, String>(Bytes::from(r#"{"xtream": "#)) })
.chain(xtream_stream)
.chain(stream::once(async { Ok::<Bytes, String>(Bytes::from(r#", "m3u": "#)) }))
.chain(m3u_stream)
.chain(stream::once(async { Ok::<Bytes, String>(Bytes::from("}")) }));
return HttpResponse::Ok()
.content_type(mime::APPLICATION_JSON)
.body(BodyStream::new(json_stream));
}
}
HttpResponse::BadRequest().finish()
@@ -75,17 +101,17 @@ async fn playlist_categories(
async fn save_playlist_bouquet(
credentials: Option<BearerAuth>,
app_state: web::Data<Arc<AppState>>,
req: web::Json<PlaylistCategoriesDto>,
req: web::Json<PlaylistBouquetDto>,
) -> HttpResponse {
if let Some(username) = get_username_from_auth_header(credentials, &app_state) {
if let Some((user, _target)) = get_user_target_by_username(username.as_str(), &app_state).await {
if !user.has_permissions(&app_state) {
return HttpResponse::Forbidden().finish();
}
match save_user_bouquet(&app_state.config, &username, &req.0).await {
match save_user_bouquet(&app_state.config, &username, &req.0) {
Ok(()) => {
return HttpResponse::Ok().finish();
},
}
Err(err) => {
error!("Saving bouquet for {username} failed: {err}");
}
@@ -104,9 +130,10 @@ async fn playlist_bouquet(
if !user.has_permissions(&app_state) {
return HttpResponse::Forbidden().finish();
}
if let Some(bouquet) = load_user_bouquet_as_json(&app_state.config, &username).await {
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(bouquet);
}
let xtream = load_user_bouquet_as_json(&app_state.config, &username, TargetType::Xtream).await;
let m3u = load_user_bouquet_as_json(&app_state.config, &username, TargetType::M3u).await;
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(
format!(r#"{{"xtream": {}, "m3u": {} }}"#, xtream.unwrap_or("null".to_string()), m3u.unwrap_or("null".to_string())));
}
}
HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{}")
+28 -14
View File
@@ -1,4 +1,5 @@
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use actix_web::body::BodyStream;
@@ -11,7 +12,7 @@ use log::error;
use serde_json::json;
use crate::api::endpoints::user_api::user_api_register;
use crate::api::endpoints::{download_api, user_api};
use crate::api::endpoints::{download_api};
use crate::api::model::app_state::AppState;
use crate::api::model::config::{ServerConfig, ServerInputConfig, ServerSourceConfig, ServerTargetConfig};
use crate::api::model::request::{PlaylistRequest, PlaylistRequestType};
@@ -20,6 +21,7 @@ use crate::m3u_filter_error::M3uFilterError;
use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, TargetUser};
use crate::model::config::{validate_targets, Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType, TargetType};
use crate::model::playlist::{XtreamCluster};
use crate::model::xtream::PlaylistXtreamCategory;
use crate::processing::processor::playlist;
use crate::repository::user_repository::store_api_user;
use crate::repository::xtream_repository;
@@ -208,13 +210,25 @@ async fn get_playlist(client: Arc<reqwest::Client>, cfg_input: Option<&ConfigInp
async fn get_categories_content(action: Result<(Option<PathBuf>, Option<String>), std::io::Error>) -> Option<String> {
if let Ok((Some(file_path), _content)) = action {
if let Ok(content) = tokio::fs::read_to_string(&file_path).await {
// TODO deserialize like sax parser
if let Ok(categories) = serde_json::from_str::<Vec<PlaylistXtreamCategory>>(&content) {
return serde_json::to_string(&categories).ok();
}
}
}
None
}
async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc<Config>) -> HttpResponse {
if let Some(target) = cfg_target {
let target_name = &target.name;
if target.has_output(&TargetType::Xtream) {
let live_categories = user_api::get_categories_content(xtream_repository::xtream_get_collection_path(cfg, target_name, xtream_repository::COL_CAT_LIVE)).await;
let vod_categories = user_api::get_categories_content(xtream_repository::xtream_get_collection_path(cfg, target_name, xtream_repository::COL_CAT_VOD)).await;
let series_categories = user_api::get_categories_content(xtream_repository::xtream_get_collection_path(cfg, target_name, xtream_repository::COL_CAT_SERIES)).await;
let live_categories = get_categories_content(xtream_repository::xtream_get_collection_path(cfg, target_name, xtream_repository::COL_CAT_LIVE)).await;
let vod_categories = get_categories_content(xtream_repository::xtream_get_collection_path(cfg, target_name, xtream_repository::COL_CAT_VOD)).await;
let series_categories = get_categories_content(xtream_repository::xtream_get_collection_path(cfg, target_name, xtream_repository::COL_CAT_SERIES)).await;
let live_channels = xtream_repository::iter_raw_xtream_playlist(cfg, target, XtreamCluster::Live);
let vod_channels = xtream_repository::iter_raw_xtream_playlist(cfg, target, XtreamCluster::Video);
@@ -226,19 +240,19 @@ async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc<Co
let json_stream =
stream::iter(vec![
Ok::<Bytes, String>(Bytes::from(r#"{"categories": {"live": "#.to_string())),
Ok::<Bytes, String>(Bytes::from(live_categories.unwrap_or("null".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#", "vod": "#.to_string())),
Ok::<Bytes, String>(Bytes::from(vod_categories.unwrap_or("null".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#", "series": "#.to_string())),
Ok::<Bytes, String>(Bytes::from(series_categories.unwrap_or("null".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#"},"channels": {"live": ["#.to_string())),
Ok::<Bytes, String>(Bytes::from(r#"{"categories": {"live": "#)),
Ok::<Bytes, String>(Bytes::from(live_categories.unwrap_or("[]".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#", "vod": "#)),
Ok::<Bytes, String>(Bytes::from(vod_categories.unwrap_or("[]".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#", "series": "#)),
Ok::<Bytes, String>(Bytes::from(series_categories.unwrap_or("[]".to_string()))),
Ok::<Bytes, String>(Bytes::from(r#"},"channels": {"live": ["#)),
]).chain(live_stream).chain(stream::iter(vec![
Ok::<Bytes, String>(Bytes::from(r#"], "vod": ["#.to_string())),
Ok::<Bytes, String>(Bytes::from(r#"], "vod": ["#)),
])).chain(vod_stream).chain(stream::iter(vec![
Ok::<Bytes, String>(Bytes::from(r#"], "series": ["#.to_string())),
Ok::<Bytes, String>(Bytes::from(r#"], "series": ["#)),
])).chain(series_stream).chain(stream::iter(vec![
Ok::<Bytes, String>(Bytes::from(r"]}}".to_string())),
Ok::<Bytes, String>(Bytes::from(r"]}}")),
]));
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(BodyStream::new(json_stream));
} else if target.has_output(&TargetType::M3u) {
+3 -3
View File
@@ -514,9 +514,9 @@ async fn xtream_player_api_handle_content_action(config: &Config, target_name: &
if let Some(file_path) = path {
// load user bouquet
let filter = match action {
ACTION_GET_LIVE_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, XtreamCluster::Live).await,
ACTION_GET_VOD_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, XtreamCluster::Video).await,
ACTION_GET_SERIES_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, XtreamCluster::Series).await,
ACTION_GET_LIVE_CATEGORIES => user_repository::user_get_bouquet_filter(config, target_name,&user.username, category_id, TargetType::Xtream, XtreamCluster::Live).await,
ACTION_GET_VOD_CATEGORIES => user_repository::user_get_bouquet_filter(config, target_name, &user.username, category_id, TargetType::Xtream, XtreamCluster::Video).await,
ACTION_GET_SERIES_CATEGORIES => user_repository::user_get_bouquet_filter(config, target_name, &user.username, category_id, TargetType::Xtream, XtreamCluster::Series).await,
_ => None
};
if let Some(flt) = filter {