mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-04 06:52:26 +02:00
Integrate local movie file into playlist
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
use crate::library::{MediaMetadata, MetadataAsyncIter, MetadataCacheEntry};
|
||||
use crate::model::{XtreamMovieData, XtreamMovieInfo, XtreamMovieInfoDetails};
|
||||
use crate::model::{AppConfig, ConfigInput};
|
||||
use serde_json::value::RawValue;
|
||||
use shared::error::TuliproxError;
|
||||
use shared::model::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster};
|
||||
use shared::utils::string_to_uuid_type;
|
||||
use std::sync::Arc;
|
||||
use std::path::Path;
|
||||
|
||||
pub async fn get_library_playlist(_client: &reqwest::Client, app_config: &Arc<AppConfig>, input: &Arc<ConfigInput>) -> (Vec<PlaylistGroup>, Vec<TuliproxError>) {
|
||||
let config = &*app_config.config.load();
|
||||
let Some(library_config) = config.library.as_ref() else { return (vec![], vec![]) };
|
||||
if !library_config.enabled { return (vec![], vec![]); }
|
||||
|
||||
let storage_path = std::path::PathBuf::from(&library_config.metadata.path);
|
||||
let mut metadata_iter = MetadataAsyncIter::new(&storage_path).await;
|
||||
let mut group_movies = PlaylistGroup {
|
||||
id: 0,
|
||||
title: library_config.playlist.movie_category.clone(),
|
||||
channels: vec![],
|
||||
xtream_cluster: XtreamCluster::Video,
|
||||
};
|
||||
let mut group_series = PlaylistGroup {
|
||||
id: 0,
|
||||
title: library_config.playlist.series_category.clone(),
|
||||
channels: vec![],
|
||||
xtream_cluster: XtreamCluster::Series,
|
||||
};
|
||||
while let Some(entry) = metadata_iter.next().await {
|
||||
match entry.metadata {
|
||||
MediaMetadata::Movie(_) => {
|
||||
let pli = to_playlist_item(&entry, &input.name, &library_config.playlist.movie_category);
|
||||
group_movies.channels.push(pli);
|
||||
},
|
||||
MediaMetadata::Series(_) => {
|
||||
let pli = to_playlist_item(&entry, &input.name, &library_config.playlist.series_category);
|
||||
group_series.channels.push(pli);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mut groups = vec![];
|
||||
if !group_movies.channels.is_empty() {
|
||||
groups.push(group_movies);
|
||||
}
|
||||
if !group_series.channels.is_empty() {
|
||||
groups.push(group_series);
|
||||
}
|
||||
|
||||
(groups, vec![])
|
||||
}
|
||||
|
||||
fn to_playlist_item(entry: &MetadataCacheEntry, input_name: &str, group_name: &str) -> PlaylistItem {
|
||||
let metadata = &entry.metadata;
|
||||
|
||||
let (xtream_cluster, item_type, additional_props): (XtreamCluster, PlaylistItemType, Option<Box<RawValue>>) = {
|
||||
match metadata {
|
||||
MediaMetadata::Movie(_) => {
|
||||
let add_props = metadata_cache_entry_to_xtream_movie_info(entry).and_then(|info| {
|
||||
let raw = serde_json::to_string(&info).ok()?;
|
||||
RawValue::from_string(raw).ok()
|
||||
});
|
||||
(XtreamCluster::Video, PlaylistItemType::Video, add_props)
|
||||
}
|
||||
MediaMetadata::Series(_s) => {
|
||||
(XtreamCluster::Series, PlaylistItemType::SeriesInfo, None)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
uuid: string_to_uuid_type(&entry.uuid),
|
||||
id: String::new(),
|
||||
virtual_id: 0,
|
||||
name: metadata.title().to_string(),
|
||||
chno: String::new(),
|
||||
logo: String::new(),
|
||||
logo_small: String::new(),
|
||||
group: group_name.to_string(),
|
||||
title: metadata.title().to_string(),
|
||||
parent_code: String::new(),
|
||||
audio_track: String::new(),
|
||||
time_shift: String::new(),
|
||||
rec: String::new(),
|
||||
url: format!("file://{}", entry.file_path),
|
||||
epg_channel_id: None,
|
||||
xtream_cluster,
|
||||
additional_properties: additional_props,
|
||||
item_type,
|
||||
category_id: 0,
|
||||
input_name: input_name.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata_cache_entry_to_xtream_movie_info(
|
||||
entry: &MetadataCacheEntry,
|
||||
) -> Option<XtreamMovieInfo> {
|
||||
let movie = match &entry.metadata {
|
||||
MediaMetadata::Movie(m) => m,
|
||||
MediaMetadata::Series(_) => return None,
|
||||
};
|
||||
|
||||
let container_extension = Path::new(&entry.file_path)
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(ToString::to_string);
|
||||
|
||||
let actor_names = movie.actors.as_ref().map(|a| a.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", "));
|
||||
|
||||
let info = XtreamMovieInfoDetails {
|
||||
kinopoisk_url: movie.tmdb_id.map(|id| format!("https://www.themoviedb.org/movie/{id}")),
|
||||
tmdb_id: movie.tmdb_id.map(|id|id.to_string()),
|
||||
|
||||
name: Some(movie.title.clone()),
|
||||
o_name: movie.original_title.clone(),
|
||||
|
||||
cover_big: movie.poster.clone(),
|
||||
movie_image: movie.poster.clone(),
|
||||
|
||||
releasedate: movie.year.map(|y| format!("{y}-01-01")),
|
||||
episode_run_time: movie.runtime,
|
||||
|
||||
youtube_trailer: None,
|
||||
director: movie.directors.as_ref().map(|d| d.join(", ")),
|
||||
actors: actor_names.clone(),
|
||||
cast: actor_names,
|
||||
genre: movie.genres.as_ref().map(|g| g.join(", ")),
|
||||
|
||||
description: movie.plot.clone(),
|
||||
plot: movie.plot.clone(),
|
||||
|
||||
age: None,
|
||||
mpaa_rating: movie.mpaa.clone(),
|
||||
|
||||
rating_count_kinopoisk: 0,
|
||||
|
||||
country: None,
|
||||
|
||||
backdrop_path: movie
|
||||
.fanart
|
||||
.as_ref()
|
||||
.map(|f| vec![f.clone()])
|
||||
.unwrap_or_default(),
|
||||
|
||||
duration_secs: movie.runtime.map(|r| (r * 60).to_string()),
|
||||
duration: movie.runtime.map(|r| {
|
||||
let h = r / 60;
|
||||
let m = r % 60;
|
||||
format!("{h:02}:{m:02}:00")
|
||||
}),
|
||||
|
||||
video: Vec::new(),
|
||||
audio: Vec::new(),
|
||||
|
||||
bitrate: 0,
|
||||
|
||||
rating: movie.rating.map(|r| format!("{r:.2}")),
|
||||
runtime: movie.runtime.map(|r| (r * 60).to_string()),
|
||||
status: None,
|
||||
};
|
||||
|
||||
let movie_data = XtreamMovieData {
|
||||
stream_id: 0,
|
||||
name: movie.title.clone(),
|
||||
|
||||
added: Some(entry.file_modified.to_string()),
|
||||
|
||||
category_id: None,
|
||||
category_ids: Vec::new(),
|
||||
|
||||
container_extension,
|
||||
custom_sid: None,
|
||||
direct_source: String::new(),
|
||||
};
|
||||
|
||||
Some(XtreamMovieInfo { info, movie_data })
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mod xtream_series;
|
||||
pub mod epg;
|
||||
mod sort;
|
||||
pub mod trakt;
|
||||
mod library;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! handle_error {
|
||||
|
||||
@@ -34,6 +34,7 @@ use shared::foundation::filter::{get_field_value, set_field_value, Filter, Value
|
||||
use shared::model::{CounterModifier, FieldGetAccessor, FieldSetAccessor, InputType, ItemField, MsgKind, PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistUpdateState, ProcessingOrder, UUIDType, XtreamCluster};
|
||||
use shared::utils::{default_as_default, hash_bytes};
|
||||
use std::time::Instant;
|
||||
use crate::processing::processor::library;
|
||||
|
||||
fn is_valid(pli: &PlaylistItem, filter: &Filter) -> bool {
|
||||
let provider = ValueProvider { pli };
|
||||
@@ -304,20 +305,21 @@ fn is_target_enabled(target: &ConfigTarget, user_targets: &ProcessTargets) -> bo
|
||||
(!user_targets.enabled && target.enabled) || (user_targets.enabled && user_targets.has_target(target.id))
|
||||
}
|
||||
|
||||
async fn playlist_download_from_input(client: &reqwest::Client, config: &Arc<Config>, input: &Arc<ConfigInput>) -> (Vec<PlaylistGroup>, Vec<TuliproxError>) {
|
||||
let working_dir = &config.working_dir;
|
||||
async fn playlist_download_from_input(client: &reqwest::Client, app_config: &Arc<AppConfig>, input: &Arc<ConfigInput>) -> (Vec<PlaylistGroup>, Vec<TuliproxError>) {
|
||||
let config = &*app_config.config.load();
|
||||
match input.input_type {
|
||||
InputType::M3u => m3u::get_m3u_playlist(client, config, input, working_dir).await,
|
||||
InputType::Xtream => xtream::get_xtream_playlist(config, client, input, working_dir).await,
|
||||
InputType::Library | InputType::M3uBatch | InputType::XtreamBatch => (vec![], vec![]),
|
||||
InputType::M3u => m3u::get_m3u_playlist(client, config, input).await,
|
||||
InputType::Xtream => xtream::get_xtream_playlist(config, client, input).await,
|
||||
InputType::M3uBatch | InputType::XtreamBatch => (vec![], vec![]),
|
||||
InputType::Library => library::get_library_playlist(client, app_config, input).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_source(client: &reqwest::Client, cfg: Arc<AppConfig>, source_idx: usize,
|
||||
async fn process_source(client: &reqwest::Client, app_config: Arc<AppConfig>, source_idx: usize,
|
||||
user_targets: Arc<ProcessTargets>, event_manager: Option<Arc<EventManager>>,
|
||||
playlist_state: Option<&Arc<PlaylistStorageState>>,
|
||||
) -> (Vec<InputStats>, Vec<TargetStats>, Vec<TuliproxError>) {
|
||||
let sources = cfg.sources.load();
|
||||
let sources = app_config.sources.load();
|
||||
let mut errors = vec![];
|
||||
let mut input_stats = HashMap::<String, InputStats>::new();
|
||||
let mut target_stats = Vec::<TargetStats>::new();
|
||||
@@ -327,12 +329,12 @@ async fn process_source(client: &reqwest::Client, cfg: Arc<AppConfig>, source_id
|
||||
let mut source_downloaded = false;
|
||||
for input in &source.inputs {
|
||||
if is_input_enabled(input, &user_targets) {
|
||||
let config = cfg.config.load();
|
||||
let working_dir = &config.working_dir;
|
||||
source_downloaded = true;
|
||||
let start_time = Instant::now();
|
||||
let (mut playlistgroups, mut error_list) = playlist_download_from_input(client, &config, input).await;
|
||||
let (mut playlistgroups, mut error_list) = playlist_download_from_input(client, &app_config, input).await;
|
||||
let (tvguide, mut tvguide_errors) = if error_list.is_empty() {
|
||||
let working_dir = &app_config.config.load().working_dir;
|
||||
|
||||
epg::get_xmltv(client, input, working_dir).await
|
||||
} else {
|
||||
(None, vec![])
|
||||
@@ -370,7 +372,7 @@ async fn process_source(client: &reqwest::Client, cfg: Arc<AppConfig>, source_id
|
||||
for target in &source.targets {
|
||||
let event_manager_clone = event_manager_clone.clone();
|
||||
if is_target_enabled(target, &user_targets) {
|
||||
match process_playlist_for_target(&cfg, client, &mut source_playlists, target, &mut input_stats, &mut errors, event_manager_clone, playlist_state).await {
|
||||
match process_playlist_for_target(&app_config, client, &mut source_playlists, target, &mut input_stats, &mut errors, event_manager_clone, playlist_state).await {
|
||||
Ok(()) => {
|
||||
target_stats.push(TargetStats::success(&target.name));
|
||||
}
|
||||
@@ -415,7 +417,11 @@ async fn process_sources(client: &reqwest::Client, config: &Arc<AppConfig>, user
|
||||
}
|
||||
let errors = Arc::new(Mutex::<Vec<TuliproxError>>::new(vec![]));
|
||||
let stats = Arc::new(Mutex::<Vec<SourceStats>>::new(vec![]));
|
||||
for (index, _) in sources.sources.iter().enumerate() {
|
||||
for (index, source) in sources.sources.iter().enumerate() {
|
||||
if !source.should_process_for_user_targets(&user_targets) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're using the file lock this way on purpose
|
||||
let source_lock_path = PathBuf::from(format!("source_{index}"));
|
||||
let Ok(update_lock) = config.file_locks.try_write_lock(&source_lock_path).await else {
|
||||
|
||||
Reference in New Issue
Block a user