From f967c8a8e9c5e7ea169f3ce98165d7a33ee79c4b Mon Sep 17 00:00:00 2001 From: Juanjo Presa Date: Mon, 4 May 2026 12:52:06 +0200 Subject: [PATCH] Remove Plex STRM export style (#740) * Remove Plex STRM export style * Address STRM removal review feedback * test: cover STRM TMDB marker variants * docs: add Plex STRM breaking change note --- CHANGELOG.md | 4 + README.md | 5 +- backend/src/repository/strm_repository.rs | 105 ++++++++---------- docs/src/configuration/source.md | 11 +- .../source_editor/output_strm_form.rs | 2 +- shared/src/model/strm_export_style.rs | 5 - 6 files changed, 59 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4acfe020..ef15dbba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## ⚠️ Breaking Changes +- Removed the `plex` STRM export style. Existing STRM outputs configured with `style: plex` must switch to `kodi`, + `emby`, or `jellyfin`; Plex use cases should use the HDHomeRun integration instead. Existing generated TMDB marker + paths remain read-compatible, but `style: plex` is no longer accepted in configuration. + ## 🌟 New Features - **Per-User Output Clusters**: API proxy users can now be restricted to specific clusters on their assigned target via diff --git a/README.md b/README.md index cea3f8087..96956cd6d 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ See [`LICENSE`](https://github.com/euzu/tuliprox/blob/develop/LICENSE). | **M3U/M3U8** | For all IPTV players (VLC, Tivimate, iMPlayer, etc.) | | **Xtream Codes API** | Full Xtream API with Live, VOD, Series, Catchup, EPG | | **HDHomeRun** | Emulation for Plex, Jellyfin, Emby — auto-discovery via SSDP | -| **STRM** | Kodi/Plex/Jellyfin compatible with multi-version support and quality tags | +| **STRM** | Kodi/Jellyfin/Emby compatible with multi-version support and quality tags | Generate all four formats simultaneously from the same source — one setup, every platform covered. @@ -265,7 +265,8 @@ Generate all four formats simultaneously from the same source — one setup, eve - Merge multiple providers into one unified playlist - Filter, rename, sort channels — exactly the way you want - Automatic EPG assignment with fuzzy matching -- Kodi/Plex/Jellyfin integration via STRM or HDHomeRun +- Kodi/Jellyfin/Emby integration via STRM +- Plex integration via HDHomeRun ### For Self-Hosted & Homelab Users diff --git a/backend/src/repository/strm_repository.rs b/backend/src/repository/strm_repository.rs index 92fabc67f..d79b8eda0 100644 --- a/backend/src/repository/strm_repository.rs +++ b/backend/src/repository/strm_repository.rs @@ -484,65 +484,6 @@ fn format_for_kodi( } } -/// Formats names according to the official Plex documentation. -/// Movie: /Movie Name (Year) {tmdb-XXXXX}/Movie Name (Year).strm -/// Series: /Show Name (Year) {tmdb-XXXXX}/Season 01/Show Name - s01e01.strm -fn format_for_plex( - strm_item_info: &StrmItemInfo, - tmdb_id: u32, - separator: &str, - flat: bool, - flat_dedup_paths: &mut HashMap, -) -> (PathBuf, String) { - // Plex ID format: {tmdb-12345} - let parts = prepare_filename_parts(strm_item_info, tmdb_id, separator, &format!("{separator}{{tmdb-{{}}}}")); - let mut dir_path = PathBuf::new(); - - match strm_item_info.item_type { - PlaylistItemType::Video | PlaylistItemType::LocalVideo => { - let folder_name = format!("{}{}", parts.base_name, parts.id_string); - let final_filename = parts.base_name; - - if flat { - if tmdb_id > 0 { - if let Some(path) = flat_dedup_paths.get(&tmdb_id) { - dir_path.clone_from(path); - } else { - dir_path.push(&folder_name); - flat_dedup_paths.insert(tmdb_id, dir_path.clone()); - } - } else { - dir_path.push(format!("{folder_name}{separator}[{}]", parts.category)); - } - } else { - dir_path.push(parts.category); - dir_path.push(folder_name); - } - (dir_path, final_filename) - } - PlaylistItemType::Series | PlaylistItemType::LocalSeries => { - let series_folder_name = format!("{}{}", parts.base_name, parts.id_string); - let season_num = strm_item_info.season.unwrap_or(1); - let episode_num = strm_item_info.episode.unwrap_or(1); - - // Plex standard: lowercase 's' and hyphens as separators. - let final_filename = format!("{} - s{season_num:02}e{episode_num:02}", parts.sanitized_name); - let season_folder = format!("Season{separator}{season_num:02}"); - - if flat { - dir_path.push(format!("{series_folder_name}{separator}[{}]", parts.category)); - dir_path.push(season_folder); - } else { - dir_path.push(parts.category); - dir_path.push(series_folder_name); - dir_path.push(season_folder); - } - (dir_path, final_filename) - } - _ => (PathBuf::new(), sanitize_for_filename(&strm_item_info.title, separator == "_")), - } -} - /// Formats names according to the official Emby documentation. /// Movie: /Movie Name (Year)/Movie Name (Year) [tmdbid=XXXXX].strm /// Series: /Show Name (Year) [tmdbid=XXXXX]/Season 01/Show Name - S01E01.strm @@ -680,7 +621,6 @@ fn style_based_rename( // Dispatch the call to the responsible function based on the style. match style { StrmExportStyle::Kodi => format_for_kodi(strm_item_info, tmdb_id, separator, flat, flat_dedup_paths), - StrmExportStyle::Plex => format_for_plex(strm_item_info, tmdb_id, separator, flat, flat_dedup_paths), StrmExportStyle::Emby => format_for_emby(strm_item_info, tmdb_id, separator, flat, flat_dedup_paths), StrmExportStyle::Jellyfin => format_for_jellyfin(strm_item_info, tmdb_id, separator, flat, flat_dedup_paths), } @@ -1279,7 +1219,7 @@ async fn remove_empty_dirs(root_path: PathBuf) { #[cfg(test)] mod tests { - use super::{resolve_strm_source_url, StrmItemInfo}; + use super::{resolve_strm_source_url, strip_tmdb_markers, strm_contains_tmdb_marker, StrmItemInfo}; use crate::model::{ConfigInput, ConfigProvider}; use shared::model::{ConfigProviderDto, InputType, PlaylistItemType, ProviderUrlSelectionPolicy}; use std::{collections::HashMap, sync::Arc}; @@ -1321,6 +1261,49 @@ mod tests { } } + #[test] + fn tmdb_marker_helpers_cover_supported_marker_variants() { + let cases = [ + ( + "Movies/Movie Name (2020) {tmdb-12345}/Movie Name (2020).strm", + "Movies/Movie Name (2020)/Movie Name (2020).strm", + ), + ( + "Movies/Movie Name (2020) {tmdb=12345}/Movie Name (2020).strm", + "Movies/Movie Name (2020)/Movie Name (2020).strm", + ), + ( + "Movies/Movie Name (2020) [tmdbid-12345]/Movie Name (2020).strm", + "Movies/Movie Name (2020)/Movie Name (2020).strm", + ), + ( + "Movies/Movie Name (2020) [tmdbid=12345]/Movie Name (2020).strm", + "Movies/Movie Name (2020)/Movie Name (2020).strm", + ), + ( + "Movies/Movie_Name_(2020)_{tmdb-12345}/Movie_Name_(2020).strm", + "Movies/Movie_Name_(2020)/Movie_Name_(2020).strm", + ), + ( + "Movies/Movie_Name_(2020)_{tmdb=12345}/Movie_Name_(2020).strm", + "Movies/Movie_Name_(2020)/Movie_Name_(2020).strm", + ), + ( + "Movies/Movie_Name_(2020)_[tmdbid-12345]/Movie_Name_(2020).strm", + "Movies/Movie_Name_(2020)/Movie_Name_(2020).strm", + ), + ( + "Movies/Movie_Name_(2020)_[tmdbid=12345]/Movie_Name_(2020).strm", + "Movies/Movie_Name_(2020)/Movie_Name_(2020).strm", + ), + ]; + + for (path, expected) in cases { + assert!(strm_contains_tmdb_marker(path)); + assert_eq!(strip_tmdb_markers(path), expected); + } + } + #[test] fn resolve_strm_source_url_resolves_provider_scheme_without_user_context() { let input = make_input_with_provider(); diff --git a/docs/src/configuration/source.md b/docs/src/configuration/source.md index 3f00c6970..d4f097135 100644 --- a/docs/src/configuration/source.md +++ b/docs/src/configuration/source.md @@ -1109,7 +1109,7 @@ output: - type: strm directory: /media/strm username: local_user - style: plex + style: jellyfin flat: true cleanup: false underscore_whitespace: false @@ -1120,7 +1120,11 @@ output: filter: 'Type = vod' ``` -Generates local `.strm` files for Plex, Emby, Jellyfin, or Kodi-based library ingestion. +Generates local `.strm` files for Emby, Jellyfin, or Kodi-based library ingestion. + +> **Upgrade note:** `style: plex` is no longer supported. Existing STRM targets must switch to `kodi`, +> `emby`, or `jellyfin`, for example `style: plex` -> `style: jellyfin`. For Plex use cases, use the +> `hdhomerun` integration instead. Leaving `style: plex` in the configuration will fail validation. #### `strm` Parameters @@ -1131,7 +1135,7 @@ Generates local `.strm` files for Plex, Emby, Jellyfin, or Kodi-based library in | `username` | String | No | | Optional username context used when generating stream references. This affects which user-specific URL or access context Tuliprox embeds into the exported `.strm` files. | | `underscore_whitespace` | Bool | No | `false` | Replaces whitespace with `_` in paths and filenames. This improves compatibility with environments or scrapers that prefer filesystem-safe, normalized naming. | | `cleanup` | Bool | No | `false` | If enabled, Tuliprox removes orphaned output files from the STRM directory. This keeps the export directory synchronized with the target, but can delete files if the directory points to an existing media folder. | -| `style` | Enum | Yes | | Naming convention for the output structure. Supported values: `kodi`, `plex`, `emby`, `jellyfin`. This affects scraper compatibility and how downstream media servers identify titles. | +| `style` | Enum | Yes | | Naming convention for the output structure. Supported values: `kodi`, `emby`, `jellyfin`. This affects scraper compatibility and how downstream media servers identify titles. | | `flat` | Bool | No | `false` | If enabled, Tuliprox creates a flatter directory structure. This changes how categories and group information are represented on disk and can simplify some media-server imports. | | `strm_props` | List | No | | Stream property lines inserted into `.strm` files, mainly for Kodi player behavior. This allows low-level playback hints to be embedded directly into generated files. | | `add_quality_to_filename` | Bool | No | `false` | Appends detected media quality tags such as `[1080p 4K HEVC HDR]` to the filename. This improves visibility in library UIs but depends on prior probing/enrichment data being available. | @@ -1140,7 +1144,6 @@ Generates local `.strm` files for Plex, Emby, Jellyfin, or Kodi-based library in #### Supported `style` Conventions * **Kodi:** `Movie Name (Year) {tmdb=ID}/Movie Name (Year).strm` -* **Plex:** `Movie Name (Year) {tmdb-ID}/Movie Name (Year).strm` * **Emby:** `Movie Name (Year) [tmdbid=ID]/Movie Name (Year).strm` * **Jellyfin:** `Movie Name (Year) [tmdbid-ID]/Movie Name (Year).strm` diff --git a/frontend/src/app/components/source_editor/output_strm_form.rs b/frontend/src/app/components/source_editor/output_strm_form.rs index e6c065179..2ab14bee7 100644 --- a/frontend/src/app/components/source_editor/output_strm_form.rs +++ b/frontend/src/app/components/source_editor/output_strm_form.rs @@ -122,7 +122,7 @@ pub fn StrmTargetOutputView(props: &StrmTargetOutputViewProps) -> Html { let export_styles = use_memo(output_form_state.form.style, |style| { let default_style = *style; - [StrmExportStyle::Kodi, StrmExportStyle::Plex, StrmExportStyle::Emby, StrmExportStyle::Jellyfin] + [StrmExportStyle::Kodi, StrmExportStyle::Emby, StrmExportStyle::Jellyfin] .iter() .map(|s| DropDownOption { id: s.to_string(), diff --git a/shared/src/model/strm_export_style.rs b/shared/src/model/strm_export_style.rs index 2e4025f60..461512b96 100644 --- a/shared/src/model/strm_export_style.rs +++ b/shared/src/model/strm_export_style.rs @@ -7,8 +7,6 @@ pub enum StrmExportStyle { #[serde(rename = "kodi")] #[default] Kodi, - #[serde(rename = "plex")] - Plex, #[serde(rename = "emby")] Emby, #[serde(rename = "jellyfin")] @@ -17,7 +15,6 @@ pub enum StrmExportStyle { impl StrmExportStyle { const KODI: &'static str = "Kodi"; - const PLEX: &'static str = "Plex"; const EMBY: &'static str = "Emby"; const JELLYFIN: &'static str = "Jellyfin"; } @@ -29,7 +26,6 @@ impl Display for StrmExportStyle { "{}", match *self { Self::Kodi => Self::KODI, - Self::Plex => Self::PLEX, Self::Emby => Self::EMBY, Self::Jellyfin => Self::JELLYFIN, } @@ -43,7 +39,6 @@ impl FromStr for StrmExportStyle { fn from_str(s: &str) -> Result { match s { Self::KODI => Ok(Self::Kodi), - Self::PLEX => Ok(Self::Plex), Self::EMBY => Ok(Self::Emby), Self::JELLYFIN => Ok(Self::Jellyfin), _ => Err(TuliproxError::Config(format!("Unknown StrmExportStyle: {s}"))),