From f999e4f69e84cb1802b30309c765df2f945438eb Mon Sep 17 00:00:00 2001 From: euzu Date: Wed, 29 Jan 2025 17:33:06 +0100 Subject: [PATCH] playlist item uuid is now calculated from input.name and provider_id --- CHANGELOG.md | 1 + src/api/api_utils.rs | 2 +- src/api/xtream_api.rs | 25 ++++---- src/model/config.rs | 6 +- src/model/playlist.rs | 65 +++++++++------------ src/model/xtream.rs | 2 +- src/processing/m3u_parser.rs | 7 +-- src/processing/playlist_processor.rs | 14 ++++- src/processing/xtream_parser.rs | 10 ++-- src/repository/id_mapping.rs | 84 --------------------------- src/repository/mod.rs | 2 + src/repository/playlist_repository.rs | 2 +- src/repository/storage.rs | 6 +- src/repository/target_id_mapping.rs | 2 +- src/repository/xtream_repository.rs | 9 +-- src/utils/hash_utils.rs | 33 +++++++++++ src/utils/mod.rs | 1 + src/utils/xtream_utils.rs | 18 ++++-- 18 files changed, 119 insertions(+), 170 deletions(-) delete mode 100644 src/repository/id_mapping.rs create mode 100644 src/utils/hash_utils.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff480074..1586784de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ # 2.1.4 (2025-01-xx) - !BREAKING CHANGE! unique `input` `name` is now mandatory, because rearranging the `source.yml` could lead to wrong results without a playlist update. - !BREAKING_CHANGE! `log_sanitize_sensitive_info` is now under `log` section as `sanitize_sensitive_info` +- !BREAKING_CHANGE! uuid generation for entries changed to `input.name` + `stream_id`. Virtual id mapping changed. The new Virtual id is not a sequence anymore. - Added Active clients count (for reverse proxy mode users) which is now displayed in `/status` and can be logged with setting `active_clients: true` under `log`section in `config.yml` ```yaml diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index d1fb5f360..dae5bb51e 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -131,7 +131,7 @@ pub async fn stream_response(app_state: &AppState, stream_url: &str, }; if let Some(stream) = stream_opt { let content_length = get_stream_content_length(provider_response.as_ref()); - let log_active_clients = app_state.config.log.as_ref().map_or(false, |l| l.active_clients); + let log_active_clients = app_state.config.log.as_ref().is_some_and(|l| l.active_clients); let stream = ActiveClientStream::new(stream, active_clients, log_active_clients); let stream_resp = if share_stream { let shared_headers = provider_response.as_ref().map_or_else(Vec::new, |(h, _)| h.clone()); diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index 4f694b6d4..9a1685afe 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -24,29 +24,24 @@ use crate::model::config::TargetType; use crate::model::config::{Config, ConfigInput, ConfigTarget}; use crate::model::playlist::{get_backdrop_path_value, FieldGetAccessor, PlaylistEntry, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; use crate::model::xtream::{INFO_RESOURCE_PREFIX, INFO_RESOURCE_PREFIX_EPISODE, PROP_BACKDROP_PATH, SEASON_RESOURCE_PREFIX}; -use crate::repository::storage::{get_target_storage_path, hash_string}; +use crate::repository::storage::{get_target_storage_path, hex_encode}; use crate::repository::target_id_mapping::TargetIdMapping; -use crate::repository::xtream_repository; use crate::repository::xtream_repository::{TAG_EPISODES, TAG_INFO_DATA, TAG_SEASONS_DATA}; +use crate::repository::xtream_repository; use crate::utils::json_utils::get_u32_from_serde_value; use crate::utils::request_utils::{extract_extension_from_url, sanitize_sensitive_info}; use crate::utils::{json_utils, request_utils, xtream_utils}; use crate::{debug_if_enabled, info_err}; - -const ACTION_GET_SERIES_INFO: &str = "get_series_info"; -const ACTION_GET_VOD_INFO: &str = "get_vod_info"; -// const ACTION_GET_LIVE_INFO: &str = "get_live_info"; +use crate::utils::hash_utils::generate_playlist_uuid; +use crate::utils::xtream_utils::{ACTION_GET_VOD_INFO, ACTION_GET_SERIES_INFO, + ACTION_GET_LIVE_CATEGORIES, ACTION_GET_VOD_CATEGORIES, + ACTION_GET_SERIES_CATEGORIES, ACTION_GET_LIVE_STREAMS, + ACTION_GET_VOD_STREAMS, + ACTION_GET_SERIES}; const ACTION_GET_EPG: &str = "get_epg"; const ACTION_GET_SHORT_EPG: &str = "get_short_epg"; const ACTION_GET_CATCHUP_TABLE: &str = "get_simple_data_table"; -const ACTION_GET_LIVE_CATEGORIES: &str = "get_live_categories"; -const ACTION_GET_VOD_CATEGORIES: &str = "get_vod_categories"; -const ACTION_GET_SERIES_CATEGORIES: &str = "get_series_categories"; -const ACTION_GET_LIVE_STREAMS: &str = "get_live_streams"; -const ACTION_GET_VOD_STREAMS: &str = "get_vod_streams"; -const ACTION_GET_SERIES: &str = "get_series"; - const TAG_ID: &str = "id"; const TAG_CATEGORY_ID: &str = "category_id"; const TAG_STREAM_ID: &str = "stream_id"; @@ -550,8 +545,8 @@ async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget for epg_list_item in epg_listings.iter_mut().filter_map(Value::as_object_mut) { // TODO epg_id if let Some(catchup_provider_id) = epg_list_item.get(TAG_ID).and_then(Value::as_str).and_then(|id| id.parse::().ok()) { - let uuid = hash_string(&format!("{}/{}", pli.url, catchup_provider_id)); - let virtual_id = target_id_mapping.insert_entry(uuid, catchup_provider_id, PlaylistItemType::Catchup, pli.provider_id); + let uuid = generate_playlist_uuid(&hex_encode(&pli.get_uuid()), &catchup_provider_id.to_string(), &pli.url); + let virtual_id = target_id_mapping.get_virtual_id(uuid, catchup_provider_id, PlaylistItemType::Catchup, pli.provider_id); epg_list_item.insert(TAG_ID.to_string(), Value::String(virtual_id.to_string())); } } diff --git a/src/model/config.rs b/src/model/config.rs index 827a1b8c8..9a79f9d44 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -10,7 +10,7 @@ use std::str::FromStr; use std::sync::{Arc, RwLock}; use crate::auth::user::UserCredential; -use log::{debug, error, info, warn}; +use log::{debug, error, warn}; use path_clean::PathClean; use url::Url; @@ -1320,8 +1320,8 @@ pub fn validate_targets(target_args: Option<&Vec>, sources: &Vec = check_targets.iter().filter(|&(_, v)| *v != 0).map(|(k, _)| k.to_string()).collect(); - info!("Processing targets {}", processing_targets.join(", ")); + // let processing_targets: Vec = check_targets.iter().filter(|&(_, v)| *v != 0).map(|(k, _)| k.to_string()).collect(); + // info!("Processing targets {}", processing_targets.join(", ")); } else { enabled = false; } diff --git a/src/model/playlist.rs b/src/model/playlist.rs index 99984e344..4556db699 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -7,11 +7,10 @@ use crate::model::api_proxy::ProxyUserCredentials; use crate::model::config::{ConfigInput, ConfigTargetOptions}; use crate::model::xmltv::TVGuide; use crate::model::xtream::{xtream_playlistitem_to_document, XtreamMappingOptions, PROP_BACKDROP_PATH, PROP_COVER}; -use crate::processing::m3u_parser::extract_id_from_url; -use crate::repository::storage::hash_string; use crate::utils::json_utils::{get_string_from_serde_value, get_u64_from_serde_value}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; +use crate::utils::hash_utils::{generate_playlist_uuid, get_provider_id}; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html @@ -20,6 +19,7 @@ pub trait PlaylistEntry { fn get_provider_id(&self) -> Option; fn get_category_id(&self) -> Option; fn get_provider_url(&self) -> Rc; + fn get_uuid(&self) -> UUIDType; } #[derive(Debug, Clone)] @@ -164,24 +164,18 @@ pub struct PlaylistItemHeader { impl PlaylistItemHeader { pub fn gen_uuid(&mut self) { - self.uuid = Rc::new(hash_string(&self.url)); + self.uuid = Rc::new(generate_playlist_uuid(&self.input_name, &self.id, &self.url)); } pub const fn get_uuid(&self) -> &Rc { &self.uuid } pub fn get_provider_id(&mut self) -> Option { - match self.id.parse::() { - Ok(id) => Some(id), - Err(_) => match extract_id_from_url(&self.url) { - Some(id) => match id.parse::() { - Ok(newid) => { - self.id = Rc::new(newid.to_string()); - Some(newid) - } - Err(_) => None, - }, - None => None, + match get_provider_id(&self.id, &self.url) { + None => None, + Some(newid) => { + self.id = Rc::new(newid.to_string()); + Some(newid) } } } @@ -331,18 +325,7 @@ impl PlaylistEntry for M3uPlaylistItem { } fn get_provider_id(&self) -> Option { - match self.provider_id.parse::() { - Ok(id) => Some(id), - Err(_) => match extract_id_from_url(&self.url) { - Some(id) => match id.parse::() { - Ok(newid) => { - Some(newid) - } - Err(_) => None, - }, - None => None, - } - } + get_provider_id(&self.provider_id, &self.url) } #[inline] fn get_category_id(&self) -> Option { @@ -352,6 +335,10 @@ impl PlaylistEntry for M3uPlaylistItem { fn get_provider_url(&self) -> Rc { Rc::clone(&self.url) } + + fn get_uuid(&self) -> UUIDType { + generate_playlist_uuid(&self.input_name, &self.provider_id, &self.url) + } } macro_rules! generate_field_accessor_impl_for_m3u_playlist_item { @@ -390,6 +377,7 @@ pub struct XtreamPlaylistItem { pub item_type: PlaylistItemType, pub category_id: u32, pub input_name: Rc, + pub channel_no: u32, } impl XtreamPlaylistItem { @@ -415,6 +403,11 @@ impl PlaylistEntry for XtreamPlaylistItem { fn get_provider_url(&self) -> Rc { Rc::clone(&self.url) } + + #[inline] + fn get_uuid(&self) -> UUIDType { + generate_playlist_uuid(&self.input_name, &self.provider_id.to_string(), &self.url) + } } pub fn get_backdrop_path_value(field: &str, value: Option<&Value>) -> Option> { @@ -525,6 +518,7 @@ impl PlaylistItem { item_type: header.item_type, category_id: header.category_id, input_name: Rc::clone(&header.input_name), + channel_no: header.chno.parse::().unwrap_or(0) } } } @@ -537,28 +531,23 @@ impl PlaylistEntry for PlaylistItem { fn get_provider_id(&self) -> Option { let header = self.header.borrow(); - match header.id.parse::() { - Ok(id) => Some(id), - Err(_) => match extract_id_from_url(&header.url) { - Some(id) => match id.parse::() { - Ok(newid) => { - Some(newid) - } - Err(_) => None, - }, - None => None, - } - } + get_provider_id(&header.id, &header.url) } #[inline] fn get_category_id(&self) -> Option { None } + #[inline] fn get_provider_url(&self) -> Rc { Rc::clone(&self.header.borrow().url) } + #[inline] + fn get_uuid(&self) -> UUIDType { + let header = self.header.borrow(); + generate_playlist_uuid(&header.input_name, &header.id, &header.url) + } } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/model/xtream.rs b/src/model/xtream.rs index 8bee6a755..f7da7c0f2 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -577,7 +577,7 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, opti ("category_id".to_string(), Value::String(format!("{}", &pli.category_id))), ("category_ids".to_string(), Value::Array(Vec::from([Value::Number(serde_json::Number::from(pli.category_id))]))), ("name".to_string(), Value::String(pli.name.as_ref().clone())), - ("num".to_string(), stream_id_value.clone()), + ("num".to_string(), Value::Number(serde_json::Number::from(pli.channel_no))), ("title".to_string(), Value::String(pli.title.as_ref().clone())), ("stream_icon".to_string(), Value::String(logo)), ]); diff --git a/src/processing/m3u_parser.rs b/src/processing/m3u_parser.rs index 00c98ab2b..01f159714 100644 --- a/src/processing/m3u_parser.rs +++ b/src/processing/m3u_parser.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use crate::model::config::{Config, ConfigInput}; use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; +use crate::utils::hash_utils::extract_id_from_url; use crate::utils::string_utils; #[inline] @@ -154,12 +155,6 @@ fn process_header(input: &ConfigInput, video_suffixes: &[&str], content: &str, u plih } -pub fn extract_id_from_url(url: &str) -> Option { - if let Some(filename) = url.split('/').next_back() { - return filename.rfind('.').map_or_else(|| Some(filename.to_string()), |index| Some(filename[..index].to_string())); - } - None -} pub fn consume_m3u<'a, I, F: FnMut(PlaylistItem)>(cfg: &Config, input: &ConfigInput, lines: I, mut visit: F) where diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index 64f7a7e8f..85294821a 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -3,7 +3,6 @@ extern crate unidecode; use crate::utils::epg_utils; use crate::utils::m3u_utils; use crate::utils::xtream_utils; -use crate::repository::storage::hash_string; use async_std::sync::Mutex; use core::cmp::Ordering; use std::cell::RefCell; @@ -131,6 +130,16 @@ fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) { } } +fn channel_no_playlist(new_playlist: &[PlaylistGroup]) { + let mut chno = 1; + for group in new_playlist { + for chan in &group.channels { + chan.header.borrow_mut().chno = Rc::new(chno.to_string()); + chno += 1; + } + } +} + fn exec_rename(pli: &PlaylistItem, rename: Option<&Vec>) { if let Some(renames) = rename { if !renames.is_empty() { @@ -440,7 +449,7 @@ fn get_processing_pipe(target: &ConfigTarget) -> ProcessingPipe { } fn duplicate_hash(item: &PlaylistItem) -> UUIDType { - hash_string(&item.get_provider_url()) + item.get_uuid() } fn execute_pipe<'a>(target: &ConfigTarget, pipe: &ProcessingPipe, fpl: &FetchedPlaylist<'a>, duplicates: &mut HashSet) -> FetchedPlaylist<'a> { @@ -541,6 +550,7 @@ async fn process_playlist_for_target(client: Arc, } else { let mut flat_new_playlist = flatten_groups(new_playlist); sort_playlist(target, &mut flat_new_playlist); + channel_no_playlist(&flat_new_playlist); map_playlist_counter(target, &flat_new_playlist); process_watch(target, cfg, &flat_new_playlist); persist_playlist(&mut flat_new_playlist, flatten_tvguide(&new_epg).as_ref(), target, cfg).await diff --git a/src/processing/xtream_parser.rs b/src/processing/xtream_parser.rs index 7644f5e0d..4ff1eee31 100644 --- a/src/processing/xtream_parser.rs +++ b/src/processing/xtream_parser.rs @@ -9,8 +9,8 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::ConfigInput; use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; use crate::model::xtream::{XtreamCategory, XtreamSeriesInfo, XtreamSeriesInfoEpisode, XtreamStream}; -use crate::repository::storage::hash_string; -use crate::utils::xtream_utils::get_xtream_stream_url_base; +use crate::utils::hash_utils::generate_playlist_uuid; +use crate::utils::xtream_utils::{get_xtream_stream_url_base, ACTION_GET_SERIES_INFO}; fn map_to_xtream_category(categories: &Value) -> Result, M3uFilterError> { match serde_json::from_value::>(categories.to_owned()) { @@ -54,7 +54,7 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s PlaylistItem { header: RefCell::new(PlaylistItemHeader { id: Rc::new(episode.id.to_string()), - uuid: Rc::new(hash_string(&episode_url)), + uuid: Rc::new(generate_playlist_uuid(&input.name, &episode.id, &episode_url)), name: Rc::new(series_name.to_string()), logo: Rc::new(episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string())), group: Rc::new(group_title.to_string()), @@ -89,7 +89,7 @@ fn create_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, p format!("{url}/movie/{username}/{password}/{}.{ext}", &stream.get_stream_id()) } XtreamCluster::Series => - format!("{}&action=get_series_info&series_id={}", get_xtream_stream_url_base(url, username, password), &stream.get_stream_id()) + format!("{}&action={ACTION_GET_SERIES_INFO}&series_id={}", get_xtream_stream_url_base(url, username, password), &stream.get_stream_id()) }; Rc::new(stream_base_url) } else { @@ -130,7 +130,7 @@ pub fn parse_xtream(input: &ConfigInput, let item = PlaylistItem { header: RefCell::new(PlaylistItemHeader { id: Rc::new(stream.get_stream_id().to_string()), - uuid: Rc::new(hash_string(&stream_url)), + uuid: Rc::new(generate_playlist_uuid(&input_name, &stream.get_stream_id().to_string(), &stream_url)), name: Rc::clone(&stream.name), logo: Rc::clone(&stream.stream_icon), group: Rc::clone(category_name), diff --git a/src/repository/id_mapping.rs b/src/repository/id_mapping.rs deleted file mode 100644 index c28aea37e..000000000 --- a/src/repository/id_mapping.rs +++ /dev/null @@ -1,84 +0,0 @@ -// This module is not included in the build - -use std::cmp::max; -use std::io::Error; -use std::path::{Path, PathBuf}; - -use log::error; -use serde::{Deserialize, Serialize}; - -use crate::repository::bplustree::BPlusTree; - -pub struct IdMapping -where - V: Serialize + for<'de> Deserialize<'de> + Clone, -{ - dirty: bool, - tree: BPlusTree, - path: PathBuf, - max_id: u32, -} - -impl IdMapping -where - V: Serialize + for<'de> Deserialize<'de> + Clone, -{ - pub fn new(path: &Path) -> Self { - let tree: BPlusTree = match BPlusTree::::deserialize(&path) { - Ok(tree) => tree, - _ => BPlusTree::::new() - }; - - let mut max_id = 0; - tree.traverse(|keys, _| { - match keys.iter().max() { - None => {} - Some(max_value) => { - max_id = max(max_id, *max_value); - } - } - }); - - IdMapping { - dirty: false, - tree, - path: path.to_path_buf(), - max_id, - } - } - - pub fn insert(&mut self, id: u32, value: V) { - self.dirty = true; - self.tree.insert(id, value); - } - - pub fn persist(&mut self) -> Result<(), Error> { - if self.dirty { - self.tree.serialize(&self.path)?; - } - self.dirty = false; - Ok(()) - } - - pub fn max_id(&self) -> u32 { - self.max_id - } - - pub fn query(&self, id: u32) -> Option<&V> { - self.tree.query(&id) - } -} - -impl Drop for IdMapping -where - V: Serialize + for<'de> Deserialize<'de> + Clone, -{ - fn drop(&mut self) { - match self.persist() { - Ok(_) => {} - Err(err) => { - error!("Failed to persist id mapping {:?} err:{}", &self.path, err.to_string()) - } - } - } -} \ No newline at end of file diff --git a/src/repository/mod.rs b/src/repository/mod.rs index b678bdc99..1d052b10e 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -3,6 +3,7 @@ pub mod target_id_mapping; pub mod bplustree; mod indexed_document; pub use indexed_document::IndexedDocumentReader; + pub mod playlist_repository; pub mod m3u_repository; pub mod xtream_repository; @@ -10,3 +11,4 @@ pub mod epg_repository; pub mod kodi_repository; pub mod m3u_playlist_iterator; pub mod xtream_playlist_iterator; + diff --git a/src/repository/playlist_repository.rs b/src/repository/playlist_repository.rs index 5c67aa919..ca0338c05 100644 --- a/src/repository/playlist_repository.rs +++ b/src/repository/playlist_repository.rs @@ -41,7 +41,7 @@ pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>, } let uuid = header.get_uuid(); let item_type = header.item_type; - header.virtual_id = target_id_mapping.insert_entry(**uuid, provider_id, item_type, 0); + header.virtual_id = target_id_mapping.get_virtual_id(**uuid, provider_id, item_type, 0); } } diff --git a/src/repository/storage.rs b/src/repository/storage.rs index c8780d7bf..b233020f6 100644 --- a/src/repository/storage.rs +++ b/src/repository/storage.rs @@ -13,12 +13,12 @@ const FILE_ID_MAPPING: &str = "id_mapping.db"; /// generates a hash from a string #[inline] -pub fn hash_string(url: &str) -> UUIDType { - blake3::hash(url.as_bytes()).into() +pub fn hash_string(text: &str) -> UUIDType { + blake3::hash(text.as_bytes()).into() } #[inline] -fn hex_encode(bytes: &[u8]) -> String { +pub fn hex_encode(bytes: &[u8]) -> String { bytes.iter().fold(String::new(), |mut output, b| { let _ = write!(output, "{b:02X}"); output diff --git a/src/repository/target_id_mapping.rs b/src/repository/target_id_mapping.rs index 725b93ac5..e858cc1ac 100644 --- a/src/repository/target_id_mapping.rs +++ b/src/repository/target_id_mapping.rs @@ -71,7 +71,7 @@ impl TargetIdMapping { } } - pub fn insert_entry(&mut self, uuid: UUIDType, provider_id: u32, item_type: PlaylistItemType, parent_virtual_id: u32) -> u32 { + pub fn get_virtual_id(&mut self, uuid: UUIDType, provider_id: u32, item_type: PlaylistItemType, parent_virtual_id: u32) -> u32 { match self.by_uuid.get(&uuid) { None => { self.dirty = true; diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index 9bef86508..1699fc284 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -1,3 +1,4 @@ +use crate::repository::storage::hex_encode; use crate::file_utils::file_reader; use crate::m3u_filter_error::str_to_io_error; use std::collections::HashMap; @@ -5,7 +6,6 @@ use std::fs; use std::fs::File; use std::io::{BufReader, Error, ErrorKind, Read}; use std::path::{Path, PathBuf}; - use log::error; use serde_json::{json, Map, Value}; @@ -16,12 +16,13 @@ use crate::model::playlist::{PlaylistEntry, PlaylistGroup, PlaylistItem, Playlis use crate::model::xtream::{rewrite_doc_urls, XtreamMappingOptions, XtreamSeriesEpisode, INFO_RESOURCE_PREFIX, INFO_RESOURCE_PREFIX_EPISODE, SEASON_RESOURCE_PREFIX}; use crate::repository::bplustree::{BPlusTree, BPlusTreeQuery, BPlusTreeUpdate}; use crate::repository::indexed_document::{IndexedDocumentDirectAccess, IndexedDocumentGarbageCollector, IndexedDocumentWriter}; -use crate::repository::storage::{get_input_storage_path, get_target_id_mapping_file, get_target_storage_path, hash_string, FILE_SUFFIX_DB, FILE_SUFFIX_INDEX}; +use crate::repository::storage::{get_input_storage_path, get_target_id_mapping_file, get_target_storage_path, FILE_SUFFIX_DB, FILE_SUFFIX_INDEX}; use crate::repository::target_id_mapping::{TargetIdMapping, VirtualIdRecord}; use crate::repository::xtream_playlist_iterator::XtreamPlaylistIterator; use crate::utils::file_utils::open_readonly_file; use crate::utils::json_utils::{get_u32_from_serde_value, json_iter_array, json_write_documents_to_file}; use crate::{create_m3u_filter_error, create_m3u_filter_error_result, info_err, notify_err}; +use crate::utils::hash_utils::generate_playlist_uuid; pub static COL_CAT_LIVE: &str = "cat_live"; pub static COL_CAT_SERIES: &str = "cat_series"; @@ -697,8 +698,8 @@ async fn rewrite_xtream_series_info

( for episode in episode_list.iter_mut().filter_map(Value::as_object_mut) { if let Some(episode_provider_id) = episode.get(TAG_ID).and_then(get_u32_from_serde_value) { - let uuid = hash_string(&format!("{provider_url}/{episode_provider_id}")); - let episode_virtual_id = target_id_mapping.insert_entry( + let uuid = generate_playlist_uuid(&hex_encode(&pli.get_uuid()), &episode_provider_id.to_string(), &provider_url); + let episode_virtual_id = target_id_mapping.get_virtual_id( uuid, episode_provider_id, PlaylistItemType::Series, diff --git a/src/utils/hash_utils.rs b/src/utils/hash_utils.rs new file mode 100644 index 000000000..dd07e1a2c --- /dev/null +++ b/src/utils/hash_utils.rs @@ -0,0 +1,33 @@ +use crate::model::playlist::UUIDType; +use crate::repository::storage::hash_string; + +pub fn extract_id_from_url(url: &str) -> Option { + if let Some(possible_id_and_ext) = url.split('/').next_back() { + return possible_id_and_ext.rfind('.').map_or_else(|| Some(possible_id_and_ext.to_string()), |index| Some(possible_id_and_ext[..index].to_string())); + } + None +} + +pub fn get_provider_id(provider_id: &str, url: &str) -> Option { + match provider_id.parse::() { + Ok(id) => Some(id), + Err(_) => match extract_id_from_url(url) { + Some(id) => match id.parse::() { + Ok(newid) => { + Some(newid) + } + Err(_) => None, + }, + None => None, + } + } +} + +pub fn generate_playlist_uuid(key: &str, provider_id: &str, url: &str) -> UUIDType { + if let Some(id) = get_provider_id(provider_id, url) { + if id > 0 { + return hash_string(&format!("{key}{id}")); + } + } + hash_string(url) +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 56803887e..496012403 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -17,6 +17,7 @@ pub mod atomic_once_flag; pub mod xtream_utils; pub mod m3u_utils; pub mod epg_utils; +pub mod hash_utils; #[macro_export] macro_rules! debug_if_enabled { diff --git a/src/utils/xtream_utils.rs b/src/utils/xtream_utils.rs index 708f6cf7a..eb3ff0a94 100644 --- a/src/utils/xtream_utils.rs +++ b/src/utils/xtream_utils.rs @@ -10,9 +10,15 @@ use log::{info, warn}; use std::cmp::Ordering; use std::io::{Error}; use crate::model::api_proxy::{ProxyUserCredentials}; -const ACTION_GET_SERIES_INFO: &str = "get_series_info"; -const ACTION_GET_VOD_INFO: &str = "get_vod_info"; -const ACTION_GET_LIVE_INFO: &str = "get_live_info"; +pub const ACTION_GET_SERIES_INFO: &str = "get_series_info"; +pub const ACTION_GET_VOD_INFO: &str = "get_vod_info"; +pub const ACTION_GET_LIVE_INFO: &str = "get_live_info"; +pub const ACTION_GET_SERIES: &str = "get_series"; +pub const ACTION_GET_LIVE_CATEGORIES: &str = "get_live_categories"; +pub const ACTION_GET_VOD_CATEGORIES: &str = "get_vod_categories"; +pub const ACTION_GET_SERIES_CATEGORIES: &str = "get_series_categories"; +pub const ACTION_GET_LIVE_STREAMS: &str = "get_live_streams"; +pub const ACTION_GET_VOD_STREAMS: &str = "get_vod_streams"; #[inline] pub fn get_xtream_stream_url_base(url: &str, username: &str, password: &str) -> String { @@ -122,9 +128,9 @@ fn get_skip_cluster(input: &ConfigInput) -> Vec { } const ACTIONS: [(XtreamCluster, &str, &str); 3] = [ - (XtreamCluster::Live, "get_live_categories", "get_live_streams"), - (XtreamCluster::Video, "get_vod_categories", "get_vod_streams"), - (XtreamCluster::Series, "get_series_categories", "get_series")]; + (XtreamCluster::Live, ACTION_GET_LIVE_CATEGORIES, ACTION_GET_LIVE_STREAMS), + (XtreamCluster::Video, ACTION_GET_VOD_CATEGORIES, ACTION_GET_VOD_STREAMS), + (XtreamCluster::Series, ACTION_GET_SERIES_CATEGORIES, ACTION_GET_SERIES)]; pub async fn get_xtream_playlist(client: Arc, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) {