mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-27 17:22:39 +01:00
Optimizations
* New Features Added series information display with episodes, cast, and metadata in playlist explorer Introduced multi-cluster playlist endpoints (live, VOD, series) for flexible content retrieval Added series info lookup and filtering capabilities Implemented sidebar navigation for source editor with dedicated pages (Main, Options, Staged, Advanced, Alias) Added filter state controls (All, Selected, Deselected) for playlist category management Enabled Trakt configuration toggle to control integration Performance Improvements Optimized B+Tree indexing for faster queries Reduced startup latency with disk-based operations and optional memory caching Improved string memory handling through efficient reference-counted strings UI Enhancements Enhanced playlist explorer with series details and episode grouping by season Added new UI icons (Options, Advanced, Staging, Alias, Trakt) Improved form layouts with toolbar controls Enhanced radio button group styling for better visual hierarchy * New Features Added series information display with episodes, cast, and metadata in playlist explorer Introduced multi-cluster playlist endpoints (live, VOD, series) for flexible content retrieval Added series info lookup and filtering capabilities Implemented sidebar navigation for source editor with dedicated pages (Main, Options, Staged, Advanced, Alias) Added filter state controls (All, Selected, Deselected) for playlist category management Enabled Trakt configuration toggle to control integration Performance Improvements Optimized B+Tree indexing for faster queries Reduced startup latency with disk-based operations and optional memory caching Improved string memory handling through efficient reference-counted strings UI Enhancements Enhanced playlist explorer with series details and episode grouping by season Added new UI icons (Options, Advanced, Staging, Alias, Trakt) Improved form layouts with toolbar controls Enhanced radio button group styling for better visual hierarchy * New Features Added series information display with episodes, cast, and metadata in playlist explorer Introduced multi-cluster playlist endpoints (live, VOD, series) for flexible content retrieval Added series info lookup and filtering capabilities Implemented sidebar navigation for source editor with dedicated pages (Main, Options, Staged, Advanced, Alias) Added filter state controls (All, Selected, Deselected) for playlist category management Enabled Trakt configuration toggle to control integration Performance Improvements Optimized B+Tree indexing for faster queries Reduced startup latency with disk-based operations and optional memory caching Improved string memory handling through efficient reference-counted strings UI Enhancements Enhanced playlist explorer with series details and episode grouping by season Added new UI icons (Options, Advanced, Staging, Alias, Trakt) Improved form layouts with toolbar controls Enhanced radio button group styling for better visual hierarchy * New Features Added series information display with episodes, cast, and metadata in playlist explorer Introduced multi-cluster playlist endpoints (live, VOD, series) for flexible content retrieval Added series info lookup and filtering capabilities Implemented sidebar navigation for source editor with dedicated pages (Main, Options, Staged, Advanced, Alias) Added filter state controls (All, Selected, Deselected) for playlist category management Enabled Trakt configuration toggle to control integration Performance Improvements Optimized B+Tree indexing for faster queries Reduced startup latency with disk-based operations and optional memory caching Improved string memory handling through efficient reference-counted strings UI Enhancements Enhanced playlist explorer with series details and episode grouping by season Added new UI icons (Options, Advanced, Staging, Alias, Trakt) Improved form layouts with toolbar controls Enhanced radio button group styling for better visual hierarchy
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::library::{MediaMetadata, MetadataAsyncIter, MetadataCacheEntry};
|
||||
use crate::model::{AppConfig, ConfigInput};
|
||||
use shared::concat_string;
|
||||
use shared::error::TuliproxError;
|
||||
use shared::model::{EpisodeStreamProperties, PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, SeriesStreamDetailEpisodeProperties, SeriesStreamDetailProperties, SeriesStreamDetailSeasonProperties, SeriesStreamProperties, StreamProperties, UUIDType, VideoStreamDetailProperties, VideoStreamProperties, XtreamCluster};
|
||||
use shared::utils::{generate_playlist_uuid, StringInterner};
|
||||
use shared::model::UUIDType;
|
||||
use shared::model::{EpisodeStreamProperties, PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, SeriesStreamDetailEpisodeProperties, SeriesStreamDetailProperties, SeriesStreamDetailSeasonProperties, SeriesStreamProperties, StreamProperties, VideoStreamDetailProperties, VideoStreamProperties, XtreamCluster};
|
||||
use shared::utils::{generate_playlist_uuid, Internable};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use shared::concat_string;
|
||||
|
||||
pub async fn download_library_playlist(_client: &reqwest::Client, app_config: &Arc<AppConfig>, input: &ConfigInput) -> (Vec<PlaylistGroup>, Vec<TuliproxError>) {
|
||||
let config = &*app_config.config.load();
|
||||
@@ -27,14 +28,13 @@ pub async fn download_library_playlist(_client: &reqwest::Client, app_config: &A
|
||||
channels: vec![],
|
||||
xtream_cluster: XtreamCluster::Series,
|
||||
};
|
||||
let mut interner = StringInterner::new();
|
||||
while let Some(entry) = metadata_iter.next().await {
|
||||
match entry.metadata {
|
||||
MediaMetadata::Movie(_) => {
|
||||
to_playlist_item(&mut interner, &entry, &input.name, &library_config.playlist.movie_category, &mut group_movies.channels);
|
||||
to_playlist_item(&entry, &input.name, &library_config.playlist.movie_category, &mut group_movies.channels);
|
||||
}
|
||||
MediaMetadata::Series(_) => {
|
||||
to_playlist_item(&mut interner, &entry, &input.name, &library_config.playlist.series_category, &mut group_series.channels);
|
||||
to_playlist_item(&entry, &input.name, &library_config.playlist.series_category, &mut group_series.channels);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,24 +51,26 @@ pub async fn download_library_playlist(_client: &reqwest::Client, app_config: &A
|
||||
}
|
||||
|
||||
|
||||
fn to_playlist_item(interner: &mut StringInterner, entry: &MetadataCacheEntry, input_name: &str, group_name: &str, channels: &mut Vec<PlaylistItem>) {
|
||||
fn to_playlist_item(entry: &MetadataCacheEntry, input_name: &Arc<str>, group_name: &str, channels: &mut Vec<PlaylistItem>) {
|
||||
let metadata = &entry.metadata;
|
||||
|
||||
match metadata {
|
||||
MediaMetadata::Movie(_) => {
|
||||
let additional_properties = metadata_cache_entry_to_xtream_movie_info(entry);
|
||||
let title = metadata.title().intern();
|
||||
let group = group_name.intern();
|
||||
channels.push(PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
uuid: UUIDType::from_valid_uuid(&entry.uuid),
|
||||
name: metadata.title().to_string(),
|
||||
group: interner.intern(group_name),
|
||||
title: metadata.title().to_string(),
|
||||
logo: metadata.poster().map_or_else(String::new, ToString::to_string),
|
||||
url: format!("file://{}", entry.file_path),
|
||||
name: Arc::clone(&title),
|
||||
title,
|
||||
group,
|
||||
logo: metadata.poster().unwrap_or("").intern(),
|
||||
url: concat_string!("file://", &entry.file_path).into(),
|
||||
xtream_cluster: XtreamCluster::Video,
|
||||
additional_properties,
|
||||
item_type: PlaylistItemType::LocalVideo,
|
||||
input_name: interner.intern(input_name),
|
||||
input_name: Arc::clone(input_name),
|
||||
..PlaylistItemHeader::default()
|
||||
}
|
||||
});
|
||||
@@ -76,30 +78,31 @@ fn to_playlist_item(interner: &mut StringInterner, entry: &MetadataCacheEntry, i
|
||||
MediaMetadata::Series(_series) => {
|
||||
if let Some(additional_properties) = metadata_cache_entry_to_xtream_series_info(entry) {
|
||||
let mut episodes = vec![];
|
||||
let group_arc: Arc<str> = group_name.intern();
|
||||
if let StreamProperties::Series(series_properties) = &additional_properties {
|
||||
if let Some(details_props) = series_properties.details.as_ref() {
|
||||
if let Some(prop_episodes) = details_props.episodes.as_ref() {
|
||||
for episode in prop_episodes {
|
||||
let logo = if episode.movie_image.is_empty() { metadata.poster().map_or_else(String::new, ToString::to_string) } else { episode.movie_image.clone() };
|
||||
let container_extension = Path::new(&episode.direct_source)
|
||||
let logo: Arc<str> = if episode.movie_image.is_empty() { metadata.poster().unwrap_or("").intern() } else { episode.movie_image.clone() };
|
||||
let container_extension = Path::new(&*episode.direct_source)
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(ToString::to_string).unwrap_or_default();
|
||||
episodes.push(PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
id: episode.id.to_string(),
|
||||
id: episode.id.to_string().into(),
|
||||
// we use parent_code for local series to find the parent series info and straighten the virtual_ids
|
||||
parent_code: entry.uuid.clone(),
|
||||
parent_code: entry.uuid.clone().into(),
|
||||
uuid: generate_playlist_uuid(input_name, &episode.id.to_string(), PlaylistItemType::LocalSeries, &episode.direct_source),
|
||||
logo: logo.clone(),
|
||||
name: episode.title.clone(),
|
||||
group: interner.intern(group_name),
|
||||
group: Arc::clone(&group_arc),
|
||||
title: episode.title.clone(),
|
||||
url: episode.direct_source.clone(),
|
||||
xtream_cluster: XtreamCluster::Series,
|
||||
item_type: PlaylistItemType::LocalSeries,
|
||||
category_id: 0,
|
||||
input_name: interner.intern(input_name),
|
||||
input_name: Arc::clone(input_name),
|
||||
additional_properties: Some(StreamProperties::Episode(EpisodeStreamProperties {
|
||||
episode_id: episode.id,
|
||||
episode: episode.episode_num,
|
||||
@@ -108,7 +111,7 @@ fn to_playlist_item(interner: &mut StringInterner, entry: &MetadataCacheEntry, i
|
||||
release_date: Some(episode.release_date.clone()),
|
||||
tmdb: episode.tmdb,
|
||||
movie_image: logo,
|
||||
container_extension,
|
||||
container_extension: container_extension.intern(),
|
||||
audio: None,
|
||||
video: None,
|
||||
})),
|
||||
@@ -123,15 +126,15 @@ fn to_playlist_item(interner: &mut StringInterner, entry: &MetadataCacheEntry, i
|
||||
let series_info = PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
uuid: UUIDType::from_valid_uuid(&entry.uuid),
|
||||
id: entry.uuid.clone(),
|
||||
name: metadata.title().to_string(),
|
||||
group: interner.intern(group_name),
|
||||
title: metadata.title().to_string(),
|
||||
logo: metadata.poster().map_or_else(String::new, ToString::to_string),
|
||||
url: format!("file://{}", entry.file_path),
|
||||
id: entry.uuid.clone().into(),
|
||||
name: metadata.title().intern(),
|
||||
group: group_name.intern(),
|
||||
title: metadata.title().intern(),
|
||||
logo: metadata.poster().unwrap_or("").intern(),
|
||||
url: concat_string!("file://", &entry.file_path).into(),
|
||||
xtream_cluster: XtreamCluster::Series,
|
||||
item_type: PlaylistItemType::LocalSeriesInfo,
|
||||
input_name: interner.intern(input_name),
|
||||
input_name: Arc::clone(input_name),
|
||||
additional_properties: Some(additional_properties),
|
||||
..PlaylistItemHeader::default()
|
||||
}
|
||||
@@ -156,64 +159,64 @@ pub fn metadata_cache_entry_to_xtream_movie_info(
|
||||
.and_then(|s| s.to_str())
|
||||
.map(ToString::to_string).unwrap_or_default();
|
||||
|
||||
let actor_names = movie.actors.as_ref().map(|a| a.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", "));
|
||||
let actor_names = movie.actors.as_ref().map(|a| a.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", ").intern());
|
||||
|
||||
let properties = VideoStreamProperties {
|
||||
name: movie.title.clone(),
|
||||
name: movie.title.clone().into(),
|
||||
category_id: 0,
|
||||
stream_id: 0,
|
||||
stream_icon: movie.poster.as_deref().or(movie.fanart.as_deref()).unwrap_or("").to_owned(),
|
||||
direct_source: String::new(),
|
||||
stream_icon: movie.poster.as_deref().or(movie.fanart.as_deref()).unwrap_or("").to_owned().into(),
|
||||
direct_source: "".into(),
|
||||
custom_sid: None,
|
||||
added: entry.file_modified.to_string(),
|
||||
container_extension,
|
||||
added: entry.file_modified.intern(),
|
||||
container_extension: container_extension.intern(),
|
||||
rating: movie.rating,
|
||||
rating_5based: None,
|
||||
stream_type: Some("movie".to_string()),
|
||||
trailer: movie.videos.as_ref().and_then(|v| v.iter().find(|video| video.site.eq_ignore_ascii_case("youtube")).map(|video| video.key.clone())),
|
||||
stream_type: Some("movie".intern()),
|
||||
trailer: movie.videos.as_ref().and_then(|v| v.iter().find(|video| video.site.eq_ignore_ascii_case("youtube")).map(|video| video.key.clone().into())),
|
||||
tmdb: movie.tmdb_id,
|
||||
is_adult: 0,
|
||||
details: Some(VideoStreamDetailProperties {
|
||||
kinopoisk_url: movie.tmdb_id.map(|id| format!("https://www.themoviedb.org/movie/{id}")),
|
||||
o_name: movie.original_title.clone(),
|
||||
cover_big: movie.poster.clone(),
|
||||
movie_image: movie.poster.clone(),
|
||||
release_date: movie.year.map(|y| format!("{y}-01-01")),
|
||||
kinopoisk_url: movie.tmdb_id.map(|id| concat_string!("https://www.themoviedb.org/movie/", &id.to_string()).into()),
|
||||
o_name: movie.original_title.clone().map(Into::into),
|
||||
cover_big: movie.poster.clone().map(Into::into),
|
||||
movie_image: movie.poster.clone().map(Into::into),
|
||||
release_date: movie.year.map(|y| format!("{y}-01-01").into()),
|
||||
episode_run_time: movie.runtime,
|
||||
director: movie.directors.as_ref().map(|d| d.join(", ")),
|
||||
youtube_trailer: movie.videos.as_ref().and_then(|v| v.iter().find(|video| video.site.eq_ignore_ascii_case("youtube")).map(|video| video.key.clone())),
|
||||
director: movie.directors.as_ref().map(|d| d.join(", ").into()),
|
||||
youtube_trailer: movie.videos.as_ref().and_then(|v| v.iter().find(|video| video.site.eq_ignore_ascii_case("youtube")).map(|video| video.key.clone().into())),
|
||||
actors: actor_names.clone(),
|
||||
cast: actor_names,
|
||||
genre: movie.genres.as_ref().map(|g| g.join(", ")),
|
||||
description: movie.plot.clone(),
|
||||
plot: movie.plot.clone(),
|
||||
cast: actor_names.clone(),
|
||||
genre: movie.genres.as_ref().map(|g| g.join(", ").into()),
|
||||
description: movie.plot.clone().map(Into::into),
|
||||
plot: movie.plot.clone().map(Into::into),
|
||||
age: None,
|
||||
mpaa_rating: movie.mpaa.clone(),
|
||||
mpaa_rating: movie.mpaa.clone().map(Into::into),
|
||||
rating_count_kinopoisk: 0,
|
||||
country: None,
|
||||
backdrop_path: movie
|
||||
.fanart
|
||||
.as_ref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|f| vec![f.clone()])
|
||||
.map(|f| vec![f.clone().into()])
|
||||
.or_else(|| {
|
||||
movie.poster
|
||||
.as_ref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|p| vec![p.clone()])
|
||||
.map(|p| vec![p.clone().into()])
|
||||
}),
|
||||
duration_secs: movie.runtime.map(|r| (r * 60).to_string()),
|
||||
duration_secs: movie.runtime.map(|r| (r * 60).to_string().into()),
|
||||
duration: movie.runtime.map(|r| {
|
||||
let h = r / 60;
|
||||
let m = r % 60;
|
||||
format!("{h:02}:{m:02}:00")
|
||||
format!("{h:02}:{m:02}:00").into()
|
||||
}),
|
||||
|
||||
video: None,
|
||||
audio: None,
|
||||
bitrate: 0,
|
||||
runtime: movie.runtime.map(|r| (r * 60).to_string()),
|
||||
status: Some("Released".to_string()),
|
||||
runtime: movie.runtime.map(|r| (r * 60).to_string().into()),
|
||||
status: Some("Released".intern()),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -229,22 +232,22 @@ pub fn metadata_cache_entry_to_xtream_series_info(
|
||||
MediaMetadata::Series(m) => m,
|
||||
};
|
||||
|
||||
let actor_names = series.actors.as_ref().map(|a| a.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", ")).unwrap_or_default();
|
||||
let actor_names: Arc<str> = series.actors.as_ref().map(|a| a.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", ")).unwrap_or_default().into();
|
||||
let release_date = series.year.map(|y| format!("{y}-01-01"));
|
||||
let youtube_trailer = series.videos.as_ref().and_then(|v| v.iter().find(|video| video.site.eq_ignore_ascii_case("youtube")).map(|video| video.key.clone())).unwrap_or_default();
|
||||
|
||||
let mut season_data = HashMap::new();
|
||||
series.seasons.as_ref().iter().for_each(|seasons| seasons.iter().for_each(|season_metadata| {
|
||||
season_data.insert(season_metadata.season_number,SeriesStreamDetailSeasonProperties {
|
||||
name: season_metadata.name.clone(),
|
||||
season_data.insert(season_metadata.season_number, SeriesStreamDetailSeasonProperties {
|
||||
name: season_metadata.name.clone().into(),
|
||||
season_number: season_metadata.season_number,
|
||||
episode_count: 0,
|
||||
overview: season_metadata.overview.clone(),
|
||||
air_date: season_metadata.air_date.clone(),
|
||||
cover: season_metadata.poster_path.clone(),
|
||||
cover_tmdb: season_metadata.poster_path.clone(),
|
||||
overview: season_metadata.overview.clone().map(Into::into),
|
||||
air_date: season_metadata.air_date.clone().map(Into::into),
|
||||
cover: season_metadata.poster_path.clone().map(Into::into),
|
||||
cover_tmdb: season_metadata.poster_path.clone().map(Into::into),
|
||||
cover_big: None,
|
||||
duration: Some(String::from("0")),
|
||||
duration: Some(String::from("0").into()),
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -258,39 +261,39 @@ pub fn metadata_cache_entry_to_xtream_series_info(
|
||||
let episode_release_date = episode.aired.as_ref().map(ToString::to_string).unwrap_or_default();
|
||||
let tmdb_id = (episode.tmdb_id > 0).then_some(episode.tmdb_id);
|
||||
|
||||
let season_entry =season_data.entry(episode.season).or_insert_with(|| {
|
||||
let season_entry = season_data.entry(episode.season).or_insert_with(|| {
|
||||
SeriesStreamDetailSeasonProperties {
|
||||
name: concat_string!(&series.title, " ", &episode.season.to_string()),
|
||||
name: concat_string!(&series.title, " ", &episode.season.to_string()).into(),
|
||||
season_number: episode.season,
|
||||
episode_count: 0,
|
||||
overview: series.poster.clone(),
|
||||
air_date: episode.aired.clone(),
|
||||
cover: series.poster.clone(),
|
||||
overview: series.poster.clone().map(Into::into),
|
||||
air_date: episode.aired.clone().map(Into::into),
|
||||
cover: series.poster.clone().map(Into::into),
|
||||
cover_tmdb: None,
|
||||
cover_big: None,
|
||||
duration: None,
|
||||
}
|
||||
});
|
||||
season_entry.episode_count = season_entry.episode_count.saturating_add(1);
|
||||
});
|
||||
season_entry.episode_count = season_entry.episode_count.saturating_add(1);
|
||||
|
||||
SeriesStreamDetailEpisodeProperties {
|
||||
id: tmdb_id.unwrap_or_default(),
|
||||
episode_num: episode.episode,
|
||||
season: episode.season,
|
||||
title: episode.title.clone(),
|
||||
container_extension,
|
||||
title: episode.title.clone().into(),
|
||||
container_extension: container_extension.into(),
|
||||
custom_sid: None,
|
||||
added: episode.file_modified.to_string(),
|
||||
direct_source: episode.file_path.clone(),
|
||||
added: episode.file_modified.to_string().into(),
|
||||
direct_source: episode.file_path.clone().into(),
|
||||
tmdb: tmdb_id,
|
||||
release_date: episode_release_date.clone(),
|
||||
plot: episode.plot.clone(),
|
||||
crew: Some(actor_names.clone()),
|
||||
release_date: episode_release_date.clone().into(),
|
||||
plot: episode.plot.clone().map(Into::into),
|
||||
crew: Some(Arc::clone(&actor_names)),
|
||||
duration_secs: episode.runtime.map_or(0, |r| r * 60),
|
||||
duration: episode.runtime
|
||||
.map(|r| format!("{:02}:{:02}:00", r / 60, r % 60))
|
||||
.unwrap_or_default(),
|
||||
movie_image: episode.thumb.clone().unwrap_or_default(),
|
||||
.unwrap_or_default().into(),
|
||||
movie_image: episode.thumb.clone().unwrap_or_default().into(),
|
||||
audio: None,
|
||||
video: None,
|
||||
bitrate: 0,
|
||||
@@ -304,30 +307,30 @@ pub fn metadata_cache_entry_to_xtream_series_info(
|
||||
seasons.sort_by_key(|s| s.season_number);
|
||||
|
||||
let properties = SeriesStreamProperties {
|
||||
name: series.title.clone(),
|
||||
name: series.title.clone().into(),
|
||||
category_id: 0,
|
||||
series_id: 0,
|
||||
backdrop_path: series
|
||||
.fanart
|
||||
.as_ref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|f| vec![f.clone()])
|
||||
.map(|f| vec![f.clone().into()])
|
||||
.or_else(|| {
|
||||
series.poster.as_ref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|p| vec![p.clone()])
|
||||
.map(|p| vec![p.clone().into()])
|
||||
}),
|
||||
cast: actor_names,
|
||||
cover: series.poster.clone().unwrap_or_default(),
|
||||
director: series.directors.as_ref().map(|d| d.join(", ")).unwrap_or_default(),
|
||||
cast: Arc::clone(&actor_names),
|
||||
cover: series.poster.clone().unwrap_or_default().into(),
|
||||
director: series.directors.as_ref().map(|d| d.join(", ")).unwrap_or_default().into(),
|
||||
episode_run_time: None,
|
||||
genre: series.genres.as_ref().map(|d| d.join(", ")),
|
||||
last_modified: Some(series.last_updated.to_string()),
|
||||
plot: series.plot.clone(),
|
||||
genre: series.genres.as_ref().map(|d| d.join(", ").into()),
|
||||
last_modified: Some(series.last_updated.to_string().into()),
|
||||
plot: series.plot.clone().map(Into::into),
|
||||
rating: series.rating.unwrap_or(0f64),
|
||||
rating_5based: 0.0,
|
||||
release_date,
|
||||
youtube_trailer,
|
||||
release_date: release_date.map(Into::into),
|
||||
youtube_trailer: youtube_trailer.into(),
|
||||
tmdb: series.tmdb_id,
|
||||
details: Some(SeriesStreamDetailProperties {
|
||||
year: series.year,
|
||||
|
||||
Reference in New Issue
Block a user