diff --git a/Cargo.lock b/Cargo.lock index fc7cc578a..51de889cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1102,7 +1102,7 @@ dependencies = [ [[package]] name = "frontend" -version = "3.2.25" +version = "3.2.26" dependencies = [ "anyhow", "base64", @@ -1114,7 +1114,7 @@ dependencies = [ "futures-signals", "gloo-render 0.2.0", "gloo-storage 0.3.0", - "gloo-timers 0.3.0", + "gloo-timers 0.2.6", "gloo-utils 0.2.0", "implicit-clone", "js-sys", @@ -1637,6 +1637,8 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" dependencies = [ + "futures-channel", + "futures-core", "js-sys", "wasm-bindgen", ] @@ -3841,7 +3843,7 @@ dependencies = [ [[package]] name = "shared" -version = "3.2.25" +version = "3.2.26" dependencies = [ "base64", "bitflags 2.10.0", @@ -4407,7 +4409,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tuliprox" -version = "3.2.25" +version = "3.2.26" dependencies = [ "arc-swap", "async-compression", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 2165556f9..b0f515d8f 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuliprox" -version = "3.2.25" +version = "3.2.26" edition = "2021" rust-version = "1.87.0" diff --git a/backend/src/model/playlist.rs b/backend/src/model/playlist.rs index 0ed9b196c..f20481bb5 100644 --- a/backend/src/model/playlist.rs +++ b/backend/src/model/playlist.rs @@ -4,14 +4,14 @@ use shared::model::{PlaylistGroup}; #[derive(Debug, Clone)] pub struct FetchedPlaylist<'a> { // Contains playlist for one input pub input: &'a ConfigInput, - pub playlistgroups: Vec, + pub playlist_groups: Vec, pub epg: Option, } impl FetchedPlaylist<'_> { pub fn update_playlist(&mut self, plg: &PlaylistGroup) { - for grp in &mut self.playlistgroups { + for grp in &mut self.playlist_groups { if grp.id == plg.id { plg.channels.iter().for_each(|item| grp.channels.push(item.clone())); return; diff --git a/backend/src/processing/parser/xtream.rs b/backend/src/processing/parser/xtream.rs index c49b6c7df..149265676 100644 --- a/backend/src/processing/parser/xtream.rs +++ b/backend/src/processing/parser/xtream.rs @@ -6,7 +6,7 @@ use crate::utils::xtream::get_xtream_stream_url_base; use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind}; use shared::model::{EpisodeStreamProperties, LiveStreamProperties, PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, SeriesStreamDetailEpisodeProperties, SeriesStreamProperties, StreamProperties, UUIDType, VideoStreamProperties, XtreamCluster}; use shared::utils::{generate_playlist_uuid, trim_last_slash}; -use std::collections::HashMap; +use indexmap::IndexMap; use tokio::task::spawn_blocking; async fn map_to_xtream_category(categories: DynReader) -> Result, TuliproxError> { @@ -136,7 +136,7 @@ pub async fn parse_xtream(input: &ConfigInput, match map_to_xtream_streams(xtream_cluster, streams).await { Ok(xtream_streams) => { - let mut group_map: HashMap = + let mut group_map: IndexMap = xtream_categories.into_iter().map(|category| (category.category_id, category)).collect(); let mut unknown_grp = XtreamCategory { category_id: 0u32, diff --git a/backend/src/processing/processor/epg.rs b/backend/src/processing/processor/epg.rs index 8da10e7da..b37f89f52 100644 --- a/backend/src/processing/processor/epg.rs +++ b/backend/src/processing/processor/epg.rs @@ -108,7 +108,7 @@ impl EpgIdCache<'_> { let smart_match_enabled = self.smart_match_enabled; let fuzzy_matching = self.fuzzy_match_enabled; - for channel in fp.playlistgroups.iter().flat_map(|g| &g.channels) { + for channel in fp.playlist_groups.iter().flat_map(|g| &g.channels) { let mut missing_epg_id = true; // insert epg_id to known channel epg_ids if let Some(id) = channel.header.epg_channel_id.as_deref() { @@ -211,7 +211,7 @@ async fn assign_channel_epg(new_epg: &mut Vec, fp: &mut FetchedPlaylist<'_> }; let filter_live = |c: &&mut PlaylistItem| c.header.xtream_cluster == XtreamCluster::Live; - fp.playlistgroups.iter_mut() + fp.playlist_groups.iter_mut() .flat_map(|g| &mut g.channels) .filter(filter_live) .for_each(assign_values); diff --git a/backend/src/processing/processor/playlist.rs b/backend/src/processing/processor/playlist.rs index 64d490f42..f8686f76a 100644 --- a/backend/src/processing/processor/playlist.rs +++ b/backend/src/processing/processor/playlist.rs @@ -1,7 +1,7 @@ use crate::model::{AppConfig, Config, ConfigFavourites, ConfigInput, ConfigRename}; -use crate::utils::epg; use crate::utils::m3u; use crate::utils::xtream; +use crate::utils::{epg, StepMeasureCallback}; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::Arc; @@ -27,13 +27,13 @@ use crate::repository::playlist_repository::{persist_input_playlist, persist_pla use crate::utils::StepMeasure; use crate::utils::{debug_if_enabled, trace_if_enabled}; use futures::StreamExt; +use indexmap::IndexMap; use log::{debug, error, info, log_enabled, trace, warn, Level}; use shared::error::{get_errors_notify_message, notify_err, TuliproxError}; use shared::foundation::filter::{get_field_value, set_field_value, Filter, ValueAccessor, ValueProvider}; use shared::model::{CounterModifier, FieldGetAccessor, FieldSetAccessor, InputType, ItemField, MsgKind, PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistItemType, PlaylistUpdateState, ProcessingOrder, UUIDType, XtreamCluster}; use shared::utils::{create_alias_uuid, default_as_default}; use std::time::Instant; -use indexmap::IndexMap; fn is_valid(pli: &PlaylistItem, filter: &Filter, match_as_ascii: bool) -> bool { let provider = ValueProvider { pli, match_as_ascii }; @@ -268,14 +268,7 @@ async fn process_source(client: &reqwest::Client, app_config: Arc, so if let Some(source) = sources.get_source_at(source_idx) { let mut source_playlists = Vec::with_capacity(128); - let broadcast_step = { - let event_manager = event_manager.clone(); - move |context: &str, msg: &str| { - if let Some(events) = &event_manager { - events.send_event(EventMessage::PlaylistUpdateProgress(context.to_owned(), msg.to_owned())); - } - } - }; + let broadcast_step = create_broadcast_callback(event_manager.as_ref()); // Download the sources let mut source_downloaded = false; @@ -284,7 +277,7 @@ async fn process_source(client: &reqwest::Client, app_config: Arc, so source_downloaded = true; let start_time = Instant::now(); // Download playlist for input - let (playlistgroups, mut error_list) = { + let (playlist_groups, mut error_list) = { broadcast_step("Playlist download", &format!("Downloading input '{}'", input.name)); let (downloaded_playlist, mut download_err) = playlist_download_from_input(client, &app_config, input).await; broadcast_step("Playlist download", &format!("Persisting input '{}' playlist", input.name)); @@ -308,10 +301,10 @@ async fn process_source(client: &reqwest::Client, app_config: Arc, so errors.append(&mut error_list); errors.append(&mut tvguide_errors); - let group_count = playlistgroups.len(); - let channel_count = playlistgroups.iter().map(|group| group.channels.len()).sum(); + let group_count = playlist_groups.len(); + let channel_count = playlist_groups.iter().map(|group| group.channels.len()).sum(); let input_name = &input.name; - if playlistgroups.is_empty() { + if playlist_groups.is_empty() { broadcast_step("Playlist download", &format!("Input '{}' playlist is empty", input.name)); info!("Source is empty {input_name}"); errors.push(notify_err!(format!("Source is empty {input_name}"))); @@ -319,7 +312,7 @@ async fn process_source(client: &reqwest::Client, app_config: Arc, so source_playlists.push( FetchedPlaylist { input, - playlistgroups, + playlist_groups, epg: tvguide, } ); @@ -334,7 +327,7 @@ async fn process_source(client: &reqwest::Client, app_config: Arc, so debug!("Source at index {source_idx} is empty"); errors.push(notify_err!(format!("Source at index {source_idx} is empty: {}", source.inputs.iter().map(|i| i.name.as_str()).collect::>().join(", ")))); } else { - debug_if_enabled!("Source has {} groups", source_playlists.iter().map(|fpl| fpl.playlistgroups.len()).sum::()); + debug_if_enabled!("Source has {} groups", source_playlists.iter().map(|fpl| fpl.playlist_groups.len()).sum::()); let event_manager_clone = event_manager.clone(); for target in &source.targets { let event_manager_clone = event_manager_clone.clone(); @@ -356,6 +349,17 @@ async fn process_source(client: &reqwest::Client, app_config: Arc, so (input_stats.into_values().collect(), target_stats, errors) } +fn create_broadcast_callback(event_manager: Option<&Arc>) -> StepMeasureCallback { + if let Some(event_mgr) = event_manager { + let events = event_mgr.clone(); + Box::new(move |context: &str, msg: &str| { + events.send_event(EventMessage::PlaylistUpdateProgress(context.to_owned(), msg.to_owned())); + }) + } else { + Box::new(move |_context: &str, _msg: &str| { /* noop */ }) + } +} + fn create_input_stat(group_count: usize, channel_count: usize, error_count: usize, input_type: InputType, input_name: &str, secs_took: u64) -> InputStats { InputStats { name: input_name.to_string(), @@ -454,22 +458,23 @@ fn duplicate_hash(item: &PlaylistItem) -> UUIDType { item.get_uuid() } -fn execute_pipe<'a>(target: &ConfigTarget, pipe: &ProcessingPipe, fpl: &FetchedPlaylist<'a>, duplicates: &mut HashSet) -> FetchedPlaylist<'a> { +fn execute_pipe<'a>(target: &ConfigTarget, pipe: &ProcessingPipe, fpl: &FetchedPlaylist<'a>, + duplicates: &mut HashSet) -> FetchedPlaylist<'a> { let mut new_fpl = FetchedPlaylist { input: fpl.input, - playlistgroups: fpl.playlistgroups.clone(), // we need to clone, because of multiple target definitions, we cant change the initial playlist. + playlist_groups: fpl.playlist_groups.clone(), // we need to clone, because of multiple target definitions, we cant change the initial playlist. epg: fpl.epg.clone(), }; if target.options.as_ref().is_some_and(|opt| opt.remove_duplicates) { - for group in &mut new_fpl.playlistgroups { + for group in &mut new_fpl.playlist_groups { // `HashSet::insert` returns true for first insert, otherweise false group.channels.retain(|item| duplicates.insert(duplicate_hash(item))); } } for f in pipe { - if let Some(groups) = f(&mut new_fpl.playlistgroups, target) { - new_fpl.playlistgroups = groups; + if let Some(groups) = f(&mut new_fpl.playlist_groups, target) { + new_fpl.playlist_groups = groups; } } new_fpl @@ -516,25 +521,19 @@ async fn process_playlist_for_target(app_config: &Arc, let mut processed_fetched_playlists: Vec = vec![]; debug!("Executing processing pipes"); - let broadcast_step = { - let event_manager = event_manager.clone(); - move |context: &str, msg: &str| { - if let Some(events) = &event_manager { - events.send_event(EventMessage::PlaylistUpdateProgress(context.to_owned(), msg.to_owned())); - } - } - }; + let broadcast_step = create_broadcast_callback(event_manager.as_ref()); 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); - playlist_resolve_series(app_config, client, target, errors, &pipe, provider_fpl, &mut processed_fpl, &step).await; + playlist_resolve_series(app_config, client, target, errors, &pipe, provider_fpl, &mut processed_fpl).await; playlist_resolve_vod(app_config, client, target, errors, &mut processed_fpl).await; // stats let input_stats = stats.get_mut(&processed_fpl.input.name); if let Some(stat) = input_stats { - stat.processed_stats.group_count = processed_fpl.playlistgroups.len(); - stat.processed_stats.channel_count = processed_fpl.playlistgroups.iter() + stat.processed_stats.group_count = processed_fpl.playlist_groups.len(); + stat.processed_stats.channel_count = processed_fpl.playlist_groups.iter() .map(|group| group.channels.len()) .sum(); } @@ -544,7 +543,10 @@ async fn process_playlist_for_target(app_config: &Arc, let (new_epg, mut new_playlist) = process_epg(&mut processed_fetched_playlists).await; step.tick("epg"); - process_favourites(&mut new_playlist, target.favourites.as_deref()); + if target.favourites.is_some() { + step.broadcast("Processing favourites for '{}' playlist", &target.name); + process_favourites(&mut new_playlist, target.favourites.as_deref()); + } if new_playlist.is_empty() { step.stop(""); @@ -642,7 +644,7 @@ async fn process_epg(processed_fetched_playlists: &mut Vec>) // we need to process each input epg. for fp in processed_fetched_playlists { process_playlist_epg(fp, &mut new_epg).await; - new_playlist.append(&mut fp.playlistgroups); + new_playlist.append(&mut fp.playlist_groups); } (new_epg, new_playlist) } diff --git a/backend/src/processing/processor/xtream_series.rs b/backend/src/processing/processor/xtream_series.rs index 510725675..d98651e6a 100644 --- a/backend/src/processing/processor/xtream_series.rs +++ b/backend/src/processing/processor/xtream_series.rs @@ -12,7 +12,6 @@ use shared::model::{InputType, PlaylistEntry, SeriesStreamProperties, StreamProp use shared::model::{PlaylistGroup, PlaylistItemType, XtreamCluster}; use std::sync::Arc; use std::time::Instant; -use crate::utils::StepMeasure; create_resolve_options_function_for_xtream_target!(series); @@ -28,7 +27,7 @@ async fn playlist_resolve_series_info(app_config: &Arc, client: &reqw } }; - let series_info_count = fpl.playlistgroups.iter() + let series_info_count = fpl.playlist_groups.iter() .flat_map(|plg| &plg.channels) .filter(|&pli| pli.header.xtream_cluster == XtreamCluster::Series && pli.header.item_type == PlaylistItemType::SeriesInfo @@ -40,7 +39,7 @@ async fn playlist_resolve_series_info(app_config: &Arc, client: &reqw let input = fpl.input; let mut result: Vec = vec![]; - for plg in &mut fpl.playlistgroups { + for plg in &mut fpl.playlist_groups { let mut group_series = vec![]; for pli in &mut plg.channels { if pli.header.xtream_cluster != XtreamCluster::Series @@ -102,15 +101,12 @@ pub async fn playlist_resolve_series(cfg: &Arc, pipe: &ProcessingPipe, provider_fpl: &mut FetchedPlaylist<'_>, processed_fpl: &mut FetchedPlaylist<'_>, - step: &StepMeasure, ) { let (resolve_series, resolve_delay) = get_resolve_series_options(target, processed_fpl); if !resolve_series { return; } - step.broadcast("Playlist resolve series info", "Resolving series info"); let series_playlist = playlist_resolve_series_info(cfg, client, errors, processed_fpl, resolve_delay).await; if series_playlist.is_empty() { return; } - step.broadcast("Playlist resolve series info", "Series info resolved, updating playlist"); // original content saved into original list for plg in &series_playlist { provider_fpl.update_playlist(plg); diff --git a/backend/src/processing/processor/xtream_vod.rs b/backend/src/processing/processor/xtream_vod.rs index b637c1a88..76a7e7fe8 100644 --- a/backend/src/processing/processor/xtream_vod.rs +++ b/backend/src/processing/processor/xtream_vod.rs @@ -10,14 +10,12 @@ use shared::model::{InputType, PlaylistEntry, StreamProperties, VideoStreamPrope use shared::model::{PlaylistItemType, XtreamCluster}; use std::sync::Arc; use std::time::Instant; -use crate::utils::StepMeasure; create_resolve_options_function_for_xtream_target!(vod); pub async fn playlist_resolve_vod(app_config: &Arc, client: &reqwest::Client, target: &ConfigTarget, errors: &mut Vec, - fpl: &mut FetchedPlaylist<'_>, - step: &StepMeasure,) { + fpl: &mut FetchedPlaylist<'_>) { let (resolve_movies, resolve_delay) = get_resolve_vod_options(target, fpl); if !resolve_movies { return; } @@ -32,7 +30,7 @@ pub async fn playlist_resolve_vod(app_config: &Arc, client: &reqwest: }; // LocalVideo entries are not resolved! - let vod_info_count = fpl.playlistgroups.iter() + let vod_info_count = fpl.playlist_groups.iter() .flat_map(|plg| &plg.channels) .filter(|pli| pli.header.xtream_cluster == XtreamCluster::Video && pli.header.item_type == PlaylistItemType::Video @@ -43,7 +41,7 @@ pub async fn playlist_resolve_vod(app_config: &Arc, client: &reqwest: let mut last_log_time = Instant::now(); let mut processed_vod_info_count = 0; - for plg in &mut fpl.playlistgroups { + for plg in &mut fpl.playlist_groups { for pli in &mut plg.channels { if pli.header.xtream_cluster != XtreamCluster::Video || pli.header.item_type != PlaylistItemType::Video diff --git a/backend/src/repository/bplustree.rs b/backend/src/repository/bplustree.rs index 8d6a58c40..d3dd23630 100644 --- a/backend/src/repository/bplustree.rs +++ b/backend/src/repository/bplustree.rs @@ -1,3 +1,4 @@ +use std::ffi::OsString; use crate::utils; use crate::utils::{binary_deserialize, binary_serialize}; use indexmap::IndexMap; @@ -8,7 +9,7 @@ use std::fs::{File, OpenOptions}; use std::io::{self, BufReader, Read, Seek, SeekFrom, Write}; use std::marker::PhantomData; use std::mem::size_of; -use std::path::{Path}; +use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; use fs2::FileExt; @@ -1839,6 +1840,19 @@ pub struct BPlusTreeUpdate { _marker_v: PhantomData, } +fn lock_path(filepath: &Path) -> PathBuf { + if let Some(stem) = filepath.file_stem() { + // filename with dot to hide + let mut name = OsString::from("."); + name.push(stem); + name.push(".lock"); + filepath.with_file_name(name) + } else { + // Fallback: without dot + filepath.with_extension("lock") + } +} + struct FileLock { // We hold the file handle to keep the advisory lock active. // When this struct is dropped, the file handle closes and OS releases the lock. @@ -1849,7 +1863,7 @@ impl FileLock { fn try_lock(filepath: &Path) -> io::Result { // Sidecar Lock Pattern: Lock a separate .lock file, not the data file itself. // This ensures implementation works on Windows where locked files cannot be renamed/deleted. - let lock_path = filepath.with_extension("lock"); + let lock_path = lock_path(filepath); let file = OpenOptions::new() .read(true) diff --git a/backend/src/repository/xtream_repository.rs b/backend/src/repository/xtream_repository.rs index 035c9fdb9..7a6eb0892 100644 --- a/backend/src/repository/xtream_repository.rs +++ b/backend/src/repository/xtream_repository.rs @@ -190,7 +190,11 @@ async fn load_old_category_ids(path: &Path) -> (u32, HashMap) { tokio::task::spawn_blocking(move || { let mut result: HashMap = HashMap::new(); let mut max_id: u32 = 0; - for (cluster, cat) in [(XtreamCluster::Live, storage_const::COL_CAT_LIVE), (XtreamCluster::Video, storage_const::COL_CAT_VOD), (XtreamCluster::Series, storage_const::COL_CAT_SERIES)] { + for (cluster, cat) in [ + (XtreamCluster::Live, storage_const::COL_CAT_LIVE), + (XtreamCluster::Video, storage_const::COL_CAT_VOD), + (XtreamCluster::Series, storage_const::COL_CAT_SERIES)] + { let col_path = get_collection_path(&old_path, cat); if col_path.exists() { if let Ok(file) = File::open(col_path) { @@ -263,7 +267,7 @@ pub async fn xtream_write_playlist( let (max_cat_id, existing_cat_ids) = load_old_category_ids(&path).await; let mut cat_id_counter = max_cat_id; for plg in playlist.iter_mut() { - if !&plg.channels.is_empty() { + if !plg.channels.is_empty() { let cat_key = format!("{}{}", plg.xtream_cluster, &plg.title); let cat_id = existing_cat_ids.get(&cat_key).unwrap_or_else(|| { cat_id_counter += 1; diff --git a/backend/src/utils/step_measure.rs b/backend/src/utils/step_measure.rs index 23b7008e6..40d8efc1a 100644 --- a/backend/src/utils/step_measure.rs +++ b/backend/src/utils/step_measure.rs @@ -41,7 +41,7 @@ impl StepMeasure { } } - pub fn broadcast(self, step: &str, msg: &str) { + pub fn broadcast(&self, step: &str, msg: &str) { (self.callback)(step, msg); } diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index c1c1478e0..7c339503c 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "frontend" -version = "3.2.25" +version = "3.2.26" edition = "2021" rust-version = "1.87.0" [dependencies] -shared = { version = "3.2.25", path = "../shared" } +shared = { version = "3.2.26", path = "../shared" } chrono = "0" yew = "0.21" yew-router = "0.18" diff --git a/frontend/public/assets/icons.json b/frontend/public/assets/icons.json index 10342a29d..cce72ae0e 100644 --- a/frontend/public/assets/icons.json +++ b/frontend/public/assets/icons.json @@ -329,7 +329,7 @@ }, { "keys": [ - "VOD" + "VOD", "Video", "Movie" ], "path": "m 19.815223,2.0000416 a 0.58850091,0.49877638 0 0 0 -0.152559,0.017775 L 2.4317328,6.0126382 A 0.58850091,0.49877638 0 0 0 2.0210941,6.6257352 L 2.9224652,9.4191448 A 1.173617,0.82462271 0 0 0 2.09356,10.207336 1.173617,0.82462271 0 0 0 2.9599692,11.002528 V 22 h 1.176613 V 21.97845 H 20.824021 V 22 h 1.175977 V 10.712682 A 0.58850091,0.49877638 0 0 0 21.411374,10.212723 H 4.4410652 a 1.173617,0.82462271 0 0 0 0,-0.0054 1.173617,0.82462271 0 0 0 -0.069287,-0.2785336 l 16.0358828,-3.7173633 0.002,0.00646 1.134023,-0.2629113 -1.157542,-3.5869872 A 0.58850091,0.49877638 0 0 0 19.815315,2.0000062 Z M 19.407126,3.1109398 20.112078,5.2966424 4.0158064,9.0280154 3.3108555,6.8423128 Z m -2.292202,0.8544558 a 0.88775572,0.75240594 0 0 0 -0.19642,0.025866 0.88775572,0.75240594 0 0 0 -0.622314,0.9244907 0.88775572,0.75240594 0 0 0 1.090798,0.5268975 0.88775572,0.75240594 0 0 0 0.621679,-0.9239538 0.88775572,0.75240594 0 0 0 -0.893743,-0.5532946 z m -3.63663,0.8426032 A 0.88775572,0.75240594 0 0 0 13.281873,4.834396 0.88775572,0.75240594 0 0 0 12.660194,5.7588867 0.88775572,0.75240594 0 0 0 13.750358,6.2857842 0.88775572,0.75240594 0 0 0 14.372672,5.3618304 0.88775572,0.75240594 0 0 0 13.478294,4.8079988 Z M 9.7698324,5.6678394 a 0.88775572,0.75240594 0 0 0 -0.19642,0.02586 0.88775572,0.75240594 0 0 0 -0.621679,0.924494 A 0.88775572,0.75240594 0 0 0 10.041896,7.1450878 0.88775572,0.75240594 0 0 0 10.664211,6.2211341 0.88775572,0.75240594 0 0 0 9.7698324,5.6678394 Z M 5.8490599,6.5767097 A 0.88775572,0.75240594 0 0 0 5.6526401,6.6031068 0.88775572,0.75240594 0 0 0 5.0303252,7.5275985 0.88775572,0.75240594 0 0 0 6.1211239,8.054495 0.88775572,0.75240594 0 0 0 6.7434384,7.1305423 0.88775572,0.75240594 0 0 0 5.8490599,6.5767097 Z M 4.1365822,11.210483 H 20.824021 V 13.47646 H 4.1365822 Z m 2.3335209,0.292541 a 0.88775572,0.75240594 0 0 0 -0.8880218,0.752092 0.88775572,0.75240594 0 0 0 0.8880218,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 4.0650679,0 a 0.88775572,0.75240594 0 0 0 -0.887386,0.752092 0.88775572,0.75240594 0 0 0 0.887386,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 3.844493,0 a 0.88775572,0.75240594 0 0 0 -0.887387,0.752092 0.88775572,0.75240594 0 0 0 0.887387,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 3.770121,0 a 0.88775572,0.75240594 0 0 0 -0.888022,0.752092 0.88775572,0.75240594 0 0 0 0.888022,0.752633 0.88775572,0.75240594 0 0 0 0.887386,-0.752633 0.88775572,0.75240594 0 0 0 -0.887386,-0.752092 z M 4.1365822,14.424658 H 20.824021 V 21.03025 H 4.1365822 Z m 5.7317781,0.759096 v 0.700912 4.107958 l 5.2219757,-2.554745 z m 1.0138837,1.402902 2.083069,0.898093 -2.083069,1.018233 z" }, diff --git a/frontend/scss/app/components/playlist/_playlist_explorer.scss b/frontend/scss/app/components/playlist/_playlist_explorer.scss index 8b5a6d0af..4b7c12c10 100644 --- a/frontend/scss/app/components/playlist/_playlist_explorer.scss +++ b/frontend/scss/app/components/playlist/_playlist_explorer.scss @@ -1,3 +1,7 @@ +$movie-card-height: 24rem; +$movie-card-width: 16rem; +$live-card-width: 280px; + .tp__playlist-explorer { display: flex; flex-flow: column; @@ -25,13 +29,20 @@ width: 100%; justify-content: space-between; - .tp__icon-button { - width: 2.5rem; - height: 2.5rem; + &-actions { + display: flex; + flex-flow: row; + gap: var(--gap-default); + align-items: center; - .svg-icon { - height: 100%; - width: 100%; + .tp__icon-button { + width: 2.5rem; + height: 2.5rem; + + .svg-icon { + height: 100%; + width: 100%; + } } } @@ -60,13 +71,43 @@ box-sizing: border-box; overflow: hidden; - &-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + &-sidebar{ + .tp__icon-button.active { + color: var(--tag-active-color); + fill: currentColor; + } + } + + &-content { + display: flex; + flex-flow: row; + width: 100%; gap: var(--gap-default); box-sizing: border-box; overflow-y: auto; + } + + &-list { + display: grid; + gap: var(--gap-default); + box-sizing: border-box; width: 100%; + + &-live { + display: grid; + grid-template-columns: repeat(auto-fill, minmax($live-card-width, 1fr)); + gap: var(--gap-default); + box-sizing: border-box; + width: 100%; + } + &-video, + &-series { + display: grid; + grid-template-columns: repeat(auto-fill, minmax($movie-card-width, 1fr)); + gap: var(--gap-default); + box-sizing: border-box; + width: 100%; + } } } @@ -81,8 +122,6 @@ border-radius: var(--border-radius); cursor: pointer; box-sizing: border-box; - max-height: 3rem; - min-height: 2.5rem; gap: var(--gap-default); padding: 0 var(--padding-default); overflow: hidden; @@ -100,8 +139,6 @@ align-items: center; cursor: pointer; box-sizing: border-box; - max-height: 3rem; - min-height: 2.5rem; gap: var(--gap-default); padding: 0 var(--padding-default); overflow: hidden; @@ -113,22 +150,19 @@ color: var(--text-button-hover-color); } - &-live:before { - content: "L"; - background-color: var(--tag-live-background-color); - border: 1px solid var(--tag-live-border-color); + &-live { + max-height: 3rem; + min-height: 3rem; } - &-video:before { - content: "V"; - background-color: var(--tag-video-background-color); - border: 1px solid var(--tag-video-border-color); + &-video { + max-height: 3rem; + min-height: 3rem; } - &-series:before { - content: "S"; - background-color: var(--tag-series-background-color); - border: 1px solid var(--tag-series-border-color); + &-series { + max-height: 3rem; + min-height: 3rem; } &-live:before, @@ -144,14 +178,76 @@ } &__channel { + display: flex; + justify-content: flex-start; + align-items: center; + fill: var(--menu-icon-color); + background-color: var(--text-button-background-color); + color: var(--text-button-color); + border: 1px solid var(--text-button-border-color); + border-radius: var(--border-radius); + cursor: pointer; + box-sizing: border-box; + gap: var(--gap-default); padding-left: var(--padding-micro); + overflow: hidden; + text-overflow: ellipsis; - &-logo { - box-sizing: border-box; - max-height: 3rem; - min-height: 2.5rem; - aspect-ratio: 1 / 1; + &-live { + position: relative; + max-height: 4rem; + min-height: 4rem; + + .tp__playlist-explorer__channel { + + &-logo { + box-sizing: border-box; + max-height: 3rem; + min-height: 3rem; + aspect-ratio: 1 / 1; + } + + &-title { + } + } } + + &-video, + &-series { + position: relative; + max-height: $movie-card-height; + min-height: $movie-card-height; + max-width: $movie-card-width; + min-width: $movie-card-width; + padding:0; + .tp__playlist-explorer__channel { + &-logo { + box-sizing: border-box; + width: 100%; + height: 100%; + aspect-ratio: 1 / 1; + } + &-title { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3rem; + background-color: var(--card-background-color); + padding-left: 2rem; + display: flex; + align-items: center; + } + } + .tp__icon-button { + position: absolute; + z-index: 1; + bottom: 0.75rem; + left: 0; + background-color: transparent; + } + } + } } \ No newline at end of file diff --git a/frontend/src/app/components/home.rs b/frontend/src/app/components/home.rs index af824b411..efb71e9e7 100644 --- a/frontend/src/app/components/home.rs +++ b/frontend/src/app/components/home.rs @@ -138,8 +138,6 @@ pub fn Home() -> Html { sources: sources.clone(), }; - - let handle_view_change = { let view_vis = view_visible.clone(); Callback::from(move |view| view_vis.set(view)) diff --git a/frontend/src/app/components/playlist/playlist_explorer.rs b/frontend/src/app/components/playlist/playlist_explorer.rs index 9f2677710..0279d61bd 100644 --- a/frontend/src/app/components/playlist/playlist_explorer.rs +++ b/frontend/src/app/components/playlist/playlist_explorer.rs @@ -10,7 +10,7 @@ use yew_i18n::use_translation; use shared::create_tuliprox_error_result; use shared::error::{TuliproxError, TuliproxErrorKind}; use shared::model::{CommonPlaylistItem, PlaylistRequest, SearchRequest, UiPlaylistGroup, XtreamCluster}; -use crate::app::components::{AppIcon, IconButton, NoContent, Search}; +use crate::app::components::{AppIcon, IconButton, NoContent, Panel, Search}; use crate::app::components::menu_item::MenuItem; use crate::app::components::popup_menu::PopupMenu; use crate::hooks::use_service_context; @@ -73,6 +73,17 @@ pub fn PlaylistExplorer() -> Html { let popup_anchor_ref = use_state(|| None::); let popup_is_open = use_state(|| false); let clipboard = use_clipboard(); + let cluster_visible = use_state(|| XtreamCluster::Live); + + let handle_cluster_change = { + let cluster_vis = cluster_visible.clone(); + Callback::from(move |(name, _event) : (String, MouseEvent)| { + if let Ok(xc) = XtreamCluster::from_str(name.as_str()) { + cluster_vis.set(xc); + } + }) + }; + let handle_popup_close = { let set_is_open = popup_is_open.clone(); @@ -239,15 +250,9 @@ pub fn PlaylistExplorer() -> Html { }) }; html! { - - { - match cluster { - XtreamCluster::Live => html! {}, - XtreamCluster::Video => html! {}, - XtreamCluster::Series => html! {}, - } - } - { group.title.clone() } + + { group.title.clone() } + } }) .collect::() @@ -261,22 +266,39 @@ pub fn PlaylistExplorer() -> Html { } else { html! {
-
- { playlist.as_ref() - .and_then(|response| response.live.as_ref()) - .map(|list| render_cluster(XtreamCluster::Live, list)) - .unwrap_or_default() - } - { playlist.as_ref() - .and_then(|response| response.vod.as_ref()) - .map(|list| render_cluster(XtreamCluster::Video, list)) - .unwrap_or_default() - } - { playlist.as_ref() - .and_then(|response| response.series.as_ref()) - .map(|list| render_cluster(XtreamCluster::Series, list)) - .unwrap_or_default() - } +
+ + + +
+
+ +
+ { playlist.as_ref() + .and_then(|response| response.live.as_ref()) + .map(|list| render_cluster(XtreamCluster::Live, list)) + .unwrap_or_default() + } +
+
+ +
+ { playlist.as_ref() + .and_then(|response| response.vod.as_ref()) + .map(|list| render_cluster(XtreamCluster::Video, list)) + .unwrap_or_default() + } +
+
+ +
+ { playlist.as_ref() + .and_then(|response| response.series.as_ref()) + .map(|list| render_cluster(XtreamCluster::Series, list)) + .unwrap_or_default() + } +
+
} @@ -288,7 +310,7 @@ pub fn PlaylistExplorer() -> Html { if logo.is_empty() { html! {} } else { - html! { () { @@ -303,19 +325,19 @@ pub fn PlaylistExplorer() -> Html { let render_group = |group: &Rc| { html! {
-
+
{ group.channels.iter().map(|chan| { let chan_clone = chan.clone(); let popup_onclick = handle_popup_onclick.clone(); html! { - + {render_channel_logo(chan)} - {chan.title.clone()} + {chan.title.clone()} } }).collect::() @@ -329,7 +351,15 @@ pub fn PlaylistExplorer() -> Html {
- +
+ + { + match *current_item { + ExplorerLevel::Categories => html!{} , + ExplorerLevel::Group(ref group) => html!{ {&group.title} }, + } + } +
diff --git a/frontend/src/app/components/playlist/playlist_explorer_page.rs b/frontend/src/app/components/playlist/playlist_explorer_page.rs index de656a10c..52cfb70c9 100644 --- a/frontend/src/app/components/playlist/playlist_explorer_page.rs +++ b/frontend/src/app/components/playlist/playlist_explorer_page.rs @@ -6,7 +6,6 @@ use shared::info_err; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum PlaylistExplorerPage { SourceSelector, - Create, } impl FromStr for PlaylistExplorerPage { @@ -15,7 +14,6 @@ impl FromStr for PlaylistExplorerPage { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "source-selector" => Ok(PlaylistExplorerPage::SourceSelector), - "create" => Ok(PlaylistExplorerPage::Create), _ => Err(info_err!(format!("Unknown page type: {s}"))), } } @@ -25,7 +23,6 @@ impl Display for PlaylistExplorerPage { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", match *self { Self::SourceSelector => "source-selector", - Self::Create => "create", }) } } \ No newline at end of file diff --git a/frontend/src/app/components/playlist/playlist_explorer_view.rs b/frontend/src/app/components/playlist/playlist_explorer_view.rs index 69681bb84..55548d883 100644 --- a/frontend/src/app/components/playlist/playlist_explorer_view.rs +++ b/frontend/src/app/components/playlist/playlist_explorer_view.rs @@ -31,7 +31,6 @@ pub fn PlaylistExplorerView() -> Html { use_effect_with(view_visible_dep, move |_| { match *view_visible { PlaylistExplorerPage::SourceSelector => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLIST_EXPLORER"), translate.t("LABEL.SOURCES")])), - PlaylistExplorerPage::Create => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLIST_EXPLORER"), translate.t("LABEL.CREATE")])), } }); }; @@ -51,9 +50,6 @@ pub fn PlaylistExplorerView() -> Html { - - {"Create"} -
> diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 049b8292c..e7a3dd67f 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shared" -version = "3.2.25" +version = "3.2.26" edition = "2021" rust-version = "1.87.0" diff --git a/shared/src/model/playlist.rs b/shared/src/model/playlist.rs index 9b7e5cec1..5137d34d0 100644 --- a/shared/src/model/playlist.rs +++ b/shared/src/model/playlist.rs @@ -1418,6 +1418,7 @@ impl PlaylistGroup { pub fn on_load(&mut self) { for pl in &mut self.channels { pl.header.gen_uuid(); + pl.header.category_id = self.id; } }