Integrate local movie file into playlist

This commit is contained in:
euzu
2025-12-16 16:43:11 +01:00
parent 15bd0ab88e
commit 37985c0324
27 changed files with 685 additions and 143 deletions
+180
View File
@@ -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 })
}
+1
View File
@@ -6,6 +6,7 @@ mod xtream_series;
pub mod epg;
mod sort;
pub mod trakt;
mod library;
#[macro_export]
macro_rules! handle_error {
+18 -12
View File
@@ -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 {