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
This commit is contained in:
Juanjo Presa
2026-05-04 12:52:06 +02:00
committed by GitHub
parent 1542cc9be8
commit f967c8a8e9
6 changed files with 59 additions and 73 deletions
+4
View File
@@ -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
+3 -2
View File
@@ -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
+44 -61
View File
@@ -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<u32, PathBuf>,
) -> (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();
+7 -4
View File
@@ -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`
@@ -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(),
-5
View File
@@ -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<Self, Self::Err> {
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}"))),