mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-04 06:52:26 +02: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:
@@ -169,9 +169,9 @@ async fn assign_channel_epg(new_epg: &mut Vec<Epg>, fp: &mut FetchedPlaylist<'_>
|
||||
let mut icon_assigned = HashSet::new();
|
||||
for epg_source in epg_sources {
|
||||
// icon tags
|
||||
let icon_tags: HashMap<&String, &Arc<XmlTag>> = epg_source.children.iter()
|
||||
let icon_tags: HashMap<&str, &Arc<XmlTag>> = epg_source.children.iter()
|
||||
.filter(|tag| tag.icon != XmlTagIcon::Undefined)
|
||||
.filter_map(|tag| tag.get_attribute_value(EPG_ATTRIB_ID).map(|id| (id, tag)))
|
||||
.filter_map(|tag| tag.get_attribute_value(EPG_ATTRIB_ID).map(|id| (id.as_str(), tag)))
|
||||
.collect();
|
||||
|
||||
let assign_values = |chan: &mut PlaylistItem| {
|
||||
@@ -180,7 +180,7 @@ async fn assign_channel_epg(new_epg: &mut Vec<Epg>, fp: &mut FetchedPlaylist<'_>
|
||||
// if the channel has no epg_id or the epg_id is not present in xmltv/tvguide then we need to match one from existing tvguide
|
||||
let not_found_in_epg = match &chan.header.epg_channel_id {
|
||||
None => true,
|
||||
Some(epg_id) => !id_cache.processed.contains(epg_id),
|
||||
Some(epg_id) => !id_cache.processed.contains(&**epg_id),
|
||||
};
|
||||
if not_found_in_epg {
|
||||
let try_match = |key: &str| {
|
||||
@@ -195,22 +195,22 @@ async fn assign_channel_epg(new_epg: &mut Vec<Epg>, fp: &mut FetchedPlaylist<'_>
|
||||
if let Some(new_id) = try_match(&chan.header.name)
|
||||
.or_else(|| chan.header.epg_channel_id.as_deref().and_then(try_match))
|
||||
{
|
||||
chan.header.epg_channel_id = Some(new_id);
|
||||
chan.header.epg_channel_id = Some(new_id.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(epg_channel_id) = chan.header.epg_channel_id.as_ref() {
|
||||
if !icon_assigned.contains(epg_channel_id) &&
|
||||
(epg_source.logo_override || chan.header.logo.is_empty() || chan.header.logo_small.is_empty()) {
|
||||
if let Some(icon_tag) = icon_tags.get(epg_channel_id) {
|
||||
if let Some(icon_tag) = icon_tags.get(&**epg_channel_id) {
|
||||
if let XmlTagIcon::Src(icon) = &icon_tag.icon {
|
||||
icon_assigned.insert(epg_channel_id.clone());
|
||||
if epg_source.logo_override || chan.header.logo.is_empty() {
|
||||
trace!("Matched channel {} to epg icon {icon}", chan.header.name);
|
||||
chan.header.logo = icon.clone();
|
||||
chan.header.logo = icon.clone().into();
|
||||
}
|
||||
if epg_source.logo_override || chan.header.logo_small.is_empty() {
|
||||
chan.header.logo_small = icon.clone();
|
||||
chan.header.logo_small = icon.clone().into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,9 +40,11 @@ use shared::foundation::filter::{get_field_value, set_field_value, Filter, Value
|
||||
use shared::model::xtream_const::XTREAM_CLUSTER;
|
||||
use shared::model::{CounterModifier, FieldGetAccessor, FieldSetAccessor, InputType, ItemField, MsgKind,
|
||||
PlaylistGroup, PlaylistItem, PlaylistItemType, PlaylistUpdateState,
|
||||
ProcessingOrder, UUIDType, XtreamCluster};
|
||||
use shared::utils::{create_alias_uuid, default_as_default, StringInterner};
|
||||
ProcessingOrder, XtreamCluster};
|
||||
use shared::utils::{create_alias_uuid, default_as_default, interner_gc, Internable};
|
||||
use std::time::Instant;
|
||||
use shared::concat_string;
|
||||
use shared::model::UUIDType;
|
||||
|
||||
fn is_valid(pli: &PlaylistItem, filter: &Filter, match_as_ascii: bool) -> bool {
|
||||
let provider = ValueProvider { pli, match_as_ascii };
|
||||
@@ -71,7 +73,7 @@ pub fn apply_filter_to_source(source: &mut dyn PlaylistSource, filter: &Filter)
|
||||
if groups.is_empty() { None } else { Some(groups.into_values().collect()) }
|
||||
}
|
||||
|
||||
fn filter_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget, _interner: &mut StringInterner) -> Option<Vec<PlaylistGroup>> {
|
||||
fn filter_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget) -> Option<Vec<PlaylistGroup>> {
|
||||
apply_filter_to_source(source, &target.filter)
|
||||
}
|
||||
|
||||
@@ -111,24 +113,24 @@ fn assign_channel_no_playlist(new_playlist: &mut [PlaylistGroup]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_rename(pli: &mut PlaylistItem, rename: Option<&Vec<ConfigRename>>, interner: &mut StringInterner) {
|
||||
fn exec_rename(pli: &mut PlaylistItem, rename: Option<&Vec<ConfigRename>>) {
|
||||
if let Some(renames) = rename {
|
||||
if !renames.is_empty() {
|
||||
let result = pli;
|
||||
for r in renames {
|
||||
let value = get_field_value(result, r.field);
|
||||
let cap = r.pattern.replace_all(&value, &r.new_name);
|
||||
if log_enabled!(log::Level::Debug) && value != cap {
|
||||
if log_enabled!(log::Level::Debug) && *value != *cap {
|
||||
trace_if_enabled!("Renamed {}={value} to {cap}", &r.field);
|
||||
}
|
||||
let value = cap.into_owned();
|
||||
set_field_value(result, r.field, value, interner);
|
||||
set_field_value(result, r.field, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rename_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget, interner: &mut StringInterner) -> Option<Vec<PlaylistGroup>> {
|
||||
fn rename_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget) -> Option<Vec<PlaylistGroup>> {
|
||||
match &target.rename {
|
||||
Some(renames) if !renames.is_empty() => {
|
||||
let mut groups: IndexMap<(XtreamCluster, Arc<str>), PlaylistGroup> = IndexMap::new();
|
||||
@@ -139,11 +141,11 @@ fn rename_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget, inter
|
||||
let value = &*pli.header.group;
|
||||
let cap = r.pattern.replace_all(value, &r.new_name);
|
||||
if *value != cap {
|
||||
pli.header.group = interner.intern(&cap);
|
||||
pli.header.group = cap.intern();
|
||||
}
|
||||
}
|
||||
}
|
||||
exec_rename(&mut pli, Some(renames), interner);
|
||||
exec_rename(&mut pli, Some(renames));
|
||||
let group_title = pli.header.group.clone();
|
||||
let cluster = pli.header.xtream_cluster;
|
||||
let cat_id = pli.header.category_id;
|
||||
@@ -197,7 +199,7 @@ fn map_channel_and_flatten(channel: PlaylistItem, mapping: &Mapping) -> Vec<Play
|
||||
result
|
||||
}
|
||||
|
||||
fn map_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget, _interner: &mut StringInterner) -> Option<Vec<PlaylistGroup>> {
|
||||
fn map_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget) -> Option<Vec<PlaylistGroup>> {
|
||||
let mapping_binding = target.mapping.load();
|
||||
let mappings = mapping_binding.as_ref()?;
|
||||
let valid_mappings = mappings.iter().filter(|m| m.mapper.as_ref().is_some_and(|v| !v.is_empty()));
|
||||
@@ -368,10 +370,9 @@ async fn playlist_download_from_input(client: &reqwest::Client, app_config: &Arc
|
||||
async fn process_source(source_idx: usize, ctx: &PlaylistProcessingContext) -> (Vec<InputStats>, Vec<TargetStats>, Vec<TuliproxError>) {
|
||||
let sources = ctx.config.sources.load();
|
||||
let mut errors = vec![];
|
||||
let mut input_stats = HashMap::<String, InputStats>::new();
|
||||
let mut input_stats = HashMap::<Arc<str>, InputStats>::new();
|
||||
let mut target_stats = Vec::<TargetStats>::new();
|
||||
if let Some(source) = sources.get_source_at(source_idx) {
|
||||
let mut interner = StringInterner::default();
|
||||
let mut source_playlists = Vec::with_capacity(128);
|
||||
let broadcast_step = create_broadcast_callback(ctx.event_manager.as_ref());
|
||||
// Download the sources
|
||||
@@ -402,7 +403,7 @@ async fn process_source(source_idx: usize, ctx: &PlaylistProcessingContext) -> (
|
||||
let tvguide = if input.input_type == InputType::Library {
|
||||
None
|
||||
} else {
|
||||
download_input_epg(ctx,input, &mut error_list).await
|
||||
download_input_epg(ctx, input, &mut error_list).await
|
||||
};
|
||||
|
||||
errors.append(&mut error_list);
|
||||
@@ -430,14 +431,13 @@ async fn process_source(source_idx: usize, ctx: &PlaylistProcessingContext) -> (
|
||||
if source_downloaded {
|
||||
if source_playlists.is_empty() {
|
||||
debug!("Source at index {source_idx} is empty");
|
||||
errors.push(notify_err!("Source at index {source_idx} is empty: {}", source.inputs.iter().map(std::string::String::as_str).collect::<Vec<&str>>().join(", ")));
|
||||
errors.push(notify_err!("Source at index {source_idx} is empty: {}", source.inputs.iter().map(Clone::clone).collect::<Vec<Arc<str>>>().join(", ")));
|
||||
} else {
|
||||
debug_if_enabled!("Source has {} groups", source_playlists.iter_mut().map(FetchedPlaylist::get_channel_count).sum::<usize>());
|
||||
for target in &source.targets {
|
||||
if is_target_enabled(target, &ctx.user_targets) {
|
||||
match process_playlist_for_target(ctx, &mut source_playlists, target,
|
||||
&mut input_stats, &mut errors,
|
||||
&mut interner).await {
|
||||
&mut input_stats, &mut errors).await {
|
||||
Ok(()) => {
|
||||
target_stats.push(TargetStats::success(&target.name));
|
||||
}
|
||||
@@ -454,7 +454,7 @@ async fn process_source(source_idx: usize, ctx: &PlaylistProcessingContext) -> (
|
||||
(input_stats.into_values().collect(), target_stats, errors)
|
||||
}
|
||||
|
||||
async fn download_input_epg(ctx: &PlaylistProcessingContext, input: &Arc<ConfigInput>,
|
||||
async fn download_input_epg(ctx: &PlaylistProcessingContext, input: &Arc<ConfigInput>,
|
||||
error_list: &mut Vec<TuliproxError>) -> Option<TVGuide> {
|
||||
// Download epg for input
|
||||
let (tvguide, mut tvguide_errors) = if error_list.is_empty() {
|
||||
@@ -468,7 +468,7 @@ async fn download_input_epg(ctx: &PlaylistProcessingContext, input: &Arc<Config
|
||||
}
|
||||
|
||||
async fn download_input(ctx: &PlaylistProcessingContext, input: &Arc<ConfigInput>)
|
||||
-> (Vec<TuliproxError>, Box<dyn PlaylistSource>, Option<TuliproxError>) {
|
||||
-> (Vec<TuliproxError>, Box<dyn PlaylistSource>, Option<TuliproxError>) {
|
||||
// Coordination Logic
|
||||
let need_download = !ctx.is_input_downloaded(&input.name).await;
|
||||
|
||||
@@ -544,8 +544,9 @@ pub struct PlaylistProcessingContext {
|
||||
pub playlist_state: Option<Arc<PlaylistStorageState>>,
|
||||
|
||||
// Coordination
|
||||
processed_inputs: Arc<Mutex<HashSet<String>>>,
|
||||
input_locks: Arc<Mutex<HashMap<String, Weak<RwLock<()>>>>>,
|
||||
processed_inputs: Arc<Mutex<HashSet<Arc<str>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
input_locks: Arc<Mutex<HashMap<Arc<str>, Weak<RwLock<()>>>>>,
|
||||
}
|
||||
|
||||
impl PlaylistProcessingContext {
|
||||
@@ -553,19 +554,19 @@ impl PlaylistProcessingContext {
|
||||
let processed = self.processed_inputs.lock().await;
|
||||
processed.contains(input_name)
|
||||
}
|
||||
pub async fn mark_input_downloaded(&self, input_name: String) -> bool {
|
||||
pub async fn mark_input_downloaded(&self, input_name: Arc<str>) -> bool {
|
||||
let mut processed = self.processed_inputs.lock().await;
|
||||
processed.insert(input_name)
|
||||
}
|
||||
|
||||
pub async fn get_input_lock(&self, input_name: &str) -> OwnedRwLockWriteGuard<()> {
|
||||
pub async fn get_input_lock(&self, input_name: &Arc<str>) -> OwnedRwLockWriteGuard<()> {
|
||||
let mut locks = self.input_locks.lock().await;
|
||||
// Try to upgrade the existing weak reference
|
||||
let lock = locks.get(input_name)
|
||||
.and_then(Weak::upgrade)
|
||||
.unwrap_or_else(|| {
|
||||
let new_lock = Arc::new(RwLock::new(()));
|
||||
locks.insert(input_name.to_string(), Arc::downgrade(&new_lock));
|
||||
locks.insert(input_name.clone(), Arc::downgrade(&new_lock));
|
||||
new_lock
|
||||
});
|
||||
|
||||
@@ -594,7 +595,7 @@ async fn process_sources(processing_ctx: &PlaylistProcessingContext) -> (Vec<Sou
|
||||
}
|
||||
|
||||
// We're using the file lock this way on purpose
|
||||
let source_lock_path = PathBuf::from(format!("source_{index}"));
|
||||
let source_lock_path = PathBuf::from(concat_string!("source_", &index.to_string()));
|
||||
let Ok(update_lock) = processing_ctx.config.file_locks.try_write_lock(&source_lock_path).await else {
|
||||
warn!("The update operation for the source at index {index} was skipped because an update is already in progress.");
|
||||
continue;
|
||||
@@ -638,7 +639,7 @@ async fn process_sources(processing_ctx: &PlaylistProcessingContext) -> (Vec<Sou
|
||||
}
|
||||
}
|
||||
|
||||
pub type ProcessingPipe = Vec<fn(source: &mut dyn PlaylistSource, target: &ConfigTarget, interner: &mut StringInterner) -> Option<Vec<PlaylistGroup>>>;
|
||||
pub type ProcessingPipe = Vec<fn(source: &mut dyn PlaylistSource, target: &ConfigTarget) -> Option<Vec<PlaylistGroup>>>;
|
||||
|
||||
fn get_processing_pipe(target: &ConfigTarget) -> ProcessingPipe {
|
||||
match &target.processing_order {
|
||||
@@ -652,8 +653,7 @@ fn get_processing_pipe(target: &ConfigTarget) -> ProcessingPipe {
|
||||
}
|
||||
|
||||
fn execute_pipe<'a>(target: &ConfigTarget, pipe: &ProcessingPipe, fpl: &FetchedPlaylist<'a>,
|
||||
duplicates: &mut HashSet<UUIDType>,
|
||||
interner: &mut StringInterner) -> FetchedPlaylist<'a> {
|
||||
duplicates: &mut HashSet<UUIDType>) -> FetchedPlaylist<'a> {
|
||||
let mut new_fpl = FetchedPlaylist {
|
||||
input: fpl.input,
|
||||
source: fpl.clone_source(),
|
||||
@@ -664,7 +664,7 @@ fn execute_pipe<'a>(target: &ConfigTarget, pipe: &ProcessingPipe, fpl: &FetchedP
|
||||
}
|
||||
|
||||
for f in pipe {
|
||||
if let Some(groups) = f(new_fpl.source.as_mut(), target, interner) {
|
||||
if let Some(groups) = f(new_fpl.source.as_mut(), target) {
|
||||
new_fpl.source = MemoryPlaylistSource::new(groups).boxed();
|
||||
}
|
||||
}
|
||||
@@ -703,9 +703,8 @@ fn flatten_groups(playlistgroups: Vec<PlaylistGroup>) -> Vec<PlaylistGroup> {
|
||||
async fn process_playlist_for_target(ctx: &PlaylistProcessingContext,
|
||||
playlists: &mut [FetchedPlaylist<'_>],
|
||||
target: &ConfigTarget,
|
||||
stats: &mut HashMap<String, InputStats>,
|
||||
stats: &mut HashMap<Arc<str>, InputStats>,
|
||||
errors: &mut Vec<TuliproxError>,
|
||||
interner: &mut StringInterner,
|
||||
) -> Result<(), Vec<TuliproxError>> {
|
||||
debug_if_enabled!("Processing order is {}", &target.processing_order);
|
||||
|
||||
@@ -719,9 +718,9 @@ async fn process_playlist_for_target(ctx: &PlaylistProcessingContext,
|
||||
let mut step = StepMeasure::new(&target.name, broadcast_step);
|
||||
for provider_fpl in playlists.iter_mut() {
|
||||
step.broadcast("Executing transformations on '{}' playlist", &target.name);
|
||||
let mut processed_fpl = execute_pipe(target, &pipe, provider_fpl, &mut duplicates, interner);
|
||||
let mut processed_fpl = execute_pipe(target, &pipe, provider_fpl, &mut duplicates);
|
||||
processed_fpl.sort_by_provider_ordinal();
|
||||
playlist_resolve_series(&ctx.config, &ctx.client, target, errors, &pipe, provider_fpl, &mut processed_fpl, interner).await;
|
||||
playlist_resolve_series(&ctx.config, &ctx.client, target, errors, &pipe, provider_fpl, &mut processed_fpl).await;
|
||||
playlist_resolve_vod(&ctx.config, &ctx.client, target, errors, provider_fpl, &mut processed_fpl).await;
|
||||
// stats
|
||||
let input_entry_name = processed_fpl.input.name.clone();
|
||||
@@ -767,7 +766,7 @@ async fn process_playlist_for_target(ctx: &PlaylistProcessingContext,
|
||||
if process_watch(&config, &ctx.client, target, &flat_new_playlist).await {
|
||||
step.tick("group watches");
|
||||
}
|
||||
let result = persist_playlist(&ctx.config, &mut flat_new_playlist, flatten_tvguide(&new_epg).as_ref(), target, ctx.playlist_state.as_ref(), interner).await;
|
||||
let result = persist_playlist(&ctx.config, &mut flat_new_playlist, flatten_tvguide(&new_epg).as_ref(), target, ctx.playlist_state.as_ref()).await;
|
||||
step.stop("Persisting playlists");
|
||||
result
|
||||
}
|
||||
@@ -866,7 +865,7 @@ async fn process_watch(cfg: &Config, client: &reqwest::Client, target: &ConfigTa
|
||||
pub async fn exec_processing(client: &reqwest::Client, app_config: Arc<AppConfig>, targets: Arc<ProcessTargets>,
|
||||
event_manager: Option<Arc<EventManager>>, playlist_state: Option<Arc<PlaylistStorageState>>,
|
||||
update_guard: Option<UpdateGuard>) {
|
||||
let _guard = if let Some(guard) = update_guard {
|
||||
let _guard = if let Some(guard) = update_guard {
|
||||
if let Some(permit) = guard.try_playlist() {
|
||||
Some(permit)
|
||||
} else {
|
||||
@@ -936,6 +935,8 @@ pub async fn exec_processing(client: &reqwest::Client, app_config: Arc<AppConfig
|
||||
if let Some(events) = &event_manager {
|
||||
events.send_event(EventMessage::PlaylistUpdateProgress("Playlist Update".to_string(), update_finished_message.clone()));
|
||||
}
|
||||
debug!("StringInterner GC removed {} strings", interner_gc());
|
||||
|
||||
info!("{update_finished_message}");
|
||||
}
|
||||
|
||||
|
||||
@@ -231,6 +231,7 @@ mod tests {
|
||||
use shared::foundation::filter::Filter;
|
||||
use shared::model::{ItemField, PlaylistItem, PlaylistItemHeader, SortOrder, SortTarget};
|
||||
use std::cmp::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn test_sort() {
|
||||
@@ -252,7 +253,7 @@ mod tests {
|
||||
.enumerate()
|
||||
.map(|(i, (name, quality))| PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
title: format!("Chanel {name} [{quality}]"),
|
||||
title: format!("Chanel {name} [{quality}]").into(),
|
||||
source_ordinal: i as u32,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -298,7 +299,7 @@ mod tests {
|
||||
"Chanel K [HD]",
|
||||
"Chanel Z [HD]",
|
||||
"Chanel T [SD]",
|
||||
];
|
||||
].into_iter().map(Into::into).collect::<Vec<Arc<str>>>();
|
||||
|
||||
let sorted = channels
|
||||
.into_iter()
|
||||
@@ -340,7 +341,7 @@ mod tests {
|
||||
.enumerate()
|
||||
.map(|(i, name)| PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
title: name.to_string(),
|
||||
title: name.to_string().into(),
|
||||
source_ordinal: i as u32,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -404,7 +405,7 @@ mod tests {
|
||||
"US| East d",
|
||||
"US| West e",
|
||||
"US| West f",
|
||||
];
|
||||
].into_iter().map(Into::into).collect::<Vec<Arc<str>>>();
|
||||
|
||||
let sorted = channels
|
||||
.into_iter()
|
||||
|
||||
@@ -4,11 +4,10 @@ use crate::utils::{extract_year_from_title, normalize_title_for_matching, TraktC
|
||||
use crate::utils::{trace_if_enabled, with};
|
||||
use log::{debug, info, trace, warn};
|
||||
use shared::error::TuliproxError;
|
||||
use shared::utils::StringInterner;
|
||||
use shared::model::{FieldGetAccessor, FieldSetAccessor, PlaylistGroup, PlaylistItem, TraktContentType, XtreamCluster};
|
||||
use shared::utils::CONSTANTS;
|
||||
use shared::utils::{Internable, CONSTANTS};
|
||||
use indexmap::IndexMap;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use strsim::normalized_levenshtein;
|
||||
|
||||
fn extract_quality(value: &str) -> Option<&str> {
|
||||
@@ -130,7 +129,6 @@ fn find_best_match_for_item<'a>(
|
||||
fn create_category_from_matches<'a>(
|
||||
matches: Vec<TraktMatchResult<'a>>,
|
||||
list_config: &'a TraktListConfig,
|
||||
interner: &mut StringInterner,
|
||||
) -> Vec<PlaylistGroup> {
|
||||
if matches.is_empty() { return vec![]; }
|
||||
|
||||
@@ -147,12 +145,12 @@ fn create_category_from_matches<'a>(
|
||||
))
|
||||
});
|
||||
|
||||
let group_title = &list_config.category_name;
|
||||
let group_title = list_config.category_name.as_str().intern();
|
||||
|
||||
for match_result in sorted_matches {
|
||||
let mut modified_item = match_result.playlist_item.clone();
|
||||
with!(mut modified_item.header => header {
|
||||
let title = header.get_field("caption").unwrap_or_else(|| Cow::Borrowed(&header.title));
|
||||
let title = header.get_field("caption").unwrap_or_else(|| Arc::clone(&header.title));
|
||||
if extract_quality(&title).is_none() {
|
||||
if let Some(quality) = extract_quality(&header.group) {
|
||||
let mut caption = String::with_capacity(title.len() + 6);
|
||||
@@ -163,7 +161,7 @@ fn create_category_from_matches<'a>(
|
||||
header.set_field("caption", &caption);
|
||||
}
|
||||
}
|
||||
header.group = interner.intern(group_title);
|
||||
header.group = group_title.clone();
|
||||
header.gen_uuid();
|
||||
matched_items_by_cluster.entry(header.xtream_cluster).or_default().push(modified_item);
|
||||
});
|
||||
@@ -172,7 +170,7 @@ fn create_category_from_matches<'a>(
|
||||
matched_items_by_cluster.into_iter().map(|(cluster, channels)| {
|
||||
PlaylistGroup {
|
||||
id: 0,
|
||||
title: interner.intern(group_title),
|
||||
title: group_title.clone(),
|
||||
channels,
|
||||
xtream_cluster: cluster,
|
||||
}
|
||||
@@ -183,7 +181,6 @@ fn match_trakt_items_with_playlist<'a>(
|
||||
trakt_items: &'a [TraktListItem],
|
||||
playlist: &'a [PlaylistGroup],
|
||||
list_config: &'a TraktListConfig,
|
||||
interner: &mut StringInterner,
|
||||
) -> Vec<PlaylistGroup> {
|
||||
let trakt_match_items: Vec<TraktMatchItem<'a>> = trakt_items
|
||||
.iter()
|
||||
@@ -207,7 +204,7 @@ fn match_trakt_items_with_playlist<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
create_category_from_matches(matches, list_config, interner)
|
||||
create_category_from_matches(matches, list_config)
|
||||
}
|
||||
|
||||
pub struct TraktCategoriesProcessor {
|
||||
@@ -234,7 +231,6 @@ impl TraktCategoriesProcessor {
|
||||
info!("Processing {} Trakt lists for target {}", trakt_config.lists.len(), target.name);
|
||||
let mut new_categories = Vec::new();
|
||||
let mut total_matches = 0;
|
||||
let mut interner = StringInterner::new();
|
||||
|
||||
for list_config in &trakt_config.lists {
|
||||
let cache_key = format!("{}:{}", list_config.user, list_config.list_slug);
|
||||
@@ -243,7 +239,7 @@ impl TraktCategoriesProcessor {
|
||||
Ok(trakt_items) => {
|
||||
debug!("Processing Trakt list {cache_key} with {} items", trakt_items.len());
|
||||
|
||||
let categories = match_trakt_items_with_playlist(&trakt_items, playlist, list_config, &mut interner);
|
||||
let categories = match_trakt_items_with_playlist(&trakt_items, playlist, list_config);
|
||||
for category in categories {
|
||||
if !category.channels.is_empty() {
|
||||
total_matches += category.channels.len();
|
||||
@@ -275,6 +271,9 @@ pub async fn process_trakt_categories_for_target(
|
||||
debug!("No Trakt configuration found for target {}", target.name);
|
||||
return Ok(None);
|
||||
};
|
||||
if !trakt_config.enabled {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let processor = TraktCategoriesProcessor::new(http_client, trakt_config);
|
||||
processor.process_trakt_categories(playlist, target, trakt_config).await
|
||||
|
||||
@@ -11,7 +11,6 @@ use log::{error, info, log_enabled, Level};
|
||||
use shared::error::TuliproxError;
|
||||
use shared::model::{InputType, PlaylistEntry, SeriesStreamProperties, StreamProperties, XtreamSeriesInfo};
|
||||
use shared::model::{PlaylistGroup, PlaylistItemType, XtreamCluster};
|
||||
use shared::utils::StringInterner;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
@@ -25,23 +24,27 @@ async fn playlist_resolve_series_info(app_config: &Arc<AppConfig>, client: &reqw
|
||||
errors: &mut Vec<TuliproxError>,
|
||||
fpl: &mut FetchedPlaylist<'_>,
|
||||
resolve_series: bool,
|
||||
resolve_delay: u16,
|
||||
interner: &mut StringInterner) -> Vec<PlaylistGroup> {
|
||||
resolve_delay: u16) -> Vec<PlaylistGroup> {
|
||||
|
||||
let input = fpl.input;
|
||||
let working_dir = &app_config.config.load().working_dir;
|
||||
let storage_path = match get_input_storage_path(&input.name, working_dir) {
|
||||
Ok(storage_path) => storage_path,
|
||||
Err(err) => {
|
||||
error!("Can't resolve vod, input storage directory for input '{}' failed: {err}", input.name);
|
||||
error!("Can't resolve series info, input storage directory for input '{}' failed: {err}", input.name);
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
let series_info_count = fpl.get_missing_series_info_count();
|
||||
|
||||
if series_info_count > 0 {
|
||||
info!("Found {series_info_count} series info to resolve");
|
||||
}
|
||||
let series_info_count = if resolve_series {
|
||||
let series_info_count = fpl.get_missing_series_info_count();
|
||||
if series_info_count > 0 {
|
||||
info!("Found {series_info_count} series info to resolve");
|
||||
}
|
||||
series_info_count
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut last_log_time = Instant::now();
|
||||
let mut processed_series_info_count = 0;
|
||||
@@ -94,7 +97,7 @@ async fn playlist_resolve_series_info(app_config: &Arc<AppConfig>, client: &reqw
|
||||
let header = &pli.header;
|
||||
(header.group.clone(), if header.name.is_empty() { header.title.clone() } else { header.name.clone() })
|
||||
};
|
||||
if let Some(episodes) = parse_xtream_series_info(&pli.get_uuid(), properties, &group, &series_name, input, interner) {
|
||||
if let Some(episodes) = parse_xtream_series_info(&pli.get_uuid(), properties, &group, &series_name, input) {
|
||||
let group = group_series.entry(pli.header.category_id)
|
||||
.or_insert_with(|| {
|
||||
PlaylistGroup {
|
||||
@@ -134,12 +137,11 @@ pub async fn playlist_resolve_series(cfg: &Arc<AppConfig>,
|
||||
pipe: &ProcessingPipe,
|
||||
provider_fpl: &mut FetchedPlaylist<'_>,
|
||||
processed_fpl: &mut FetchedPlaylist<'_>,
|
||||
interner: &mut StringInterner,
|
||||
) {
|
||||
let (resolve_series, resolve_delay) = get_resolve_series_options(target, processed_fpl);
|
||||
|
||||
provider_fpl.source.release_resources(XtreamCluster::Series);
|
||||
let series_playlist = playlist_resolve_series_info(cfg, client, errors, processed_fpl, resolve_series, resolve_delay, interner).await;
|
||||
let series_playlist = playlist_resolve_series_info(cfg, client, errors, processed_fpl, resolve_series, resolve_delay).await;
|
||||
provider_fpl.source.obtain_resources().await;
|
||||
if series_playlist.is_empty() { return; }
|
||||
|
||||
@@ -153,7 +155,7 @@ pub async fn playlist_resolve_series(cfg: &Arc<AppConfig>,
|
||||
let mut new_playlist = series_playlist;
|
||||
for f in pipe {
|
||||
let mut source = MemoryPlaylistSource::new(new_playlist);
|
||||
if let Some(v) = f(&mut source, target, interner) {
|
||||
if let Some(v) = f(&mut source, target) {
|
||||
new_playlist = v;
|
||||
} else {
|
||||
new_playlist = source.take_groups();
|
||||
|
||||
Reference in New Issue
Block a user