diff --git a/.dockerignore b/.dockerignore index 7fd7123ad..6c320ff38 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ # Ignore build artifacts target/ bin/ +debug/ # Ignore the entire docker directory... docker/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 31e328dcc..6141bb1ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Output filters are applied after all transformations have been performed, theref - Telegram message thread support. thread id can now be appended to chat-id like `chat-id:thread-id`. - Telegram supports markdown generation for structured json messages. simply set `markdown: true` in telegram config. - Added User-Stream-Connections Table to WebUI +- Enhanced STRM output filenames to include detailed media quality info (e.g., 4K, HDR, x265, 5.1) for easy version distinction. # 3.1.7 (2025-10-10) - Added Dark/Bright theme switch diff --git a/README.md b/README.md index e02307a26..b1c7ce5a1 100644 --- a/README.md +++ b/README.md @@ -856,7 +856,8 @@ Each format has different properties. - cleanup: _optional_, true|false, default false - style: _mandatory_, kodi|plex|emby|jellyfin - flat: _optional_, true|false, default false -- strm_props: _optional_, list of strings, +- strm_props: _optional_, list of strings +- add_quality_to_filename: _optional_, true|false - filter: optional filter `hdhomerun` @@ -904,11 +905,12 @@ regardless of the number of clients. Increasing the buffer size above 1024 will For example, with a buffer size of 2024, memory usage is at least 24 MB for **each** shared channel. `strm` output has additional options: -- `underscore_whitespace`: replaces all whitespaces with `_` in the path -- `cleanup`: deletes the directory given at `filename`. Don't point at existing media folder or everything will be deleted -- `style`: determines naming convention (kodi, plex, emby, jellyfin) -- `flat`: creates flat directory structure with category tags in folder names -- `strm_props`: list of properties written to the strm file +- `underscore_whitespace`: Replaces all whitespaces with `_` in the path and filename. +- `cleanup`: If `true`, the directory given at `filename` will be deleted. Don't point at existing media folder or everything will be deleted! +- `style`: Naming style convention for your media player / server (kodi, plex, emby, jellyfin) +- `flat`: If `true`, creates flat directory structure with category tags in folder names +- `strm_props`: List of stream properties placed within .strm file to configure how Kodi's internal player handles the media stream. +- `add_quality_to_filename`: If `true`, adds media quality tags to the filename (e.g., `Movie Title - [1080p|x265|HDR].strm`). Supported styles: - Kodi: `Movie Name (Year) {tmdb=ID}/Movie Name (Year).strm` diff --git a/backend/src/model/config/target.rs b/backend/src/model/config/target.rs index 6401281ed..0df33e76b 100644 --- a/backend/src/model/config/target.rs +++ b/backend/src/model/config/target.rs @@ -109,6 +109,7 @@ impl From<&M3uTargetOutput> for M3uTargetOutputDto { #[derive(Debug, Clone)] +#[allow(clippy::struct_excessive_bools)] pub struct StrmTargetOutput { pub directory: String, pub username: Option, @@ -118,6 +119,8 @@ pub struct StrmTargetOutput { pub cleanup: bool, pub strm_props: Option>, pub filter: Option, + // boolean flag to enable or disable quality info in filenames. + pub add_quality_to_filename: bool, } macros::from_impl!(StrmTargetOutput); @@ -132,6 +135,7 @@ impl From<&StrmTargetOutputDto> for StrmTargetOutput { cleanup: dto.cleanup, strm_props: dto.strm_props.clone(), filter: dto.t_filter.clone(), + add_quality_to_filename: dto.add_quality_to_filename, } } } @@ -147,6 +151,7 @@ impl From<&StrmTargetOutput> for StrmTargetOutputDto { strm_props: instance.strm_props.clone(), filter: instance.filter.as_ref().map(ToString::to_string), t_filter: instance.filter.clone(), + add_quality_to_filename: instance.add_quality_to_filename, } } } diff --git a/backend/src/model/media_properties.rs b/backend/src/model/media_properties.rs new file mode 100644 index 000000000..1f8b725e2 --- /dev/null +++ b/backend/src/model/media_properties.rs @@ -0,0 +1,255 @@ +// backend/src/model/media_properties.rs + +use serde::{Deserialize, Serialize}; +use std::fmt; +use serde_json::{Value, Map}; + +// Enum for Video Resolution +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum VideoResolution { + #[default] + Unknown, + SD, + P720, + P1080, + P2160, // 4K + P4320, // 8K +} + +impl fmt::Display for VideoResolution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VideoResolution::SD => write!(f, "SD"), + VideoResolution::P720 => write!(f, "720p"), + VideoResolution::P1080 => write!(f, "1080p"), + VideoResolution::P2160 => write!(f, "4K"), + VideoResolution::P4320 => write!(f, "8K"), + VideoResolution::Unknown => write!(f, "Unknown"), + } + } +} + +// Enum for Video Codec +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum VideoCodec { + #[default] + Other, + H264, + H265, + MPEG4, + VC1, +} + +impl fmt::Display for VideoCodec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VideoCodec::H264 => write!(f, "x264"), + VideoCodec::H265 => write!(f, "x265"), + VideoCodec::MPEG4 => write!(f, "MPEG4"), + VideoCodec::VC1 => write!(f, "VC-1"), + VideoCodec::Other => write!(f, "Other"), + } + } +} + +// Enum for Audio Codec +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum AudioCodec { + #[default] + Other, + AAC, + AC3, + EAC3, + DTS, + TrueHD, + FLAC, +} + +impl fmt::Display for AudioCodec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AudioCodec::AAC => write!(f, "AAC"), + AudioCodec::AC3 => write!(f, "AC3"), + AudioCodec::EAC3 => write!(f, "E-AC3"), + AudioCodec::DTS => write!(f, "DTS"), + AudioCodec::TrueHD => write!(f, "TrueHD"), + AudioCodec::FLAC => write!(f, "FLAC"), + AudioCodec::Other => write!(f, "Other"), + } + } +} + +// Enum for Audio Channels +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum AudioChannels { + #[default] + Unknown, + Mono, + Stereo, + Surround51, + Surround71, +} + +impl fmt::Display for AudioChannels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AudioChannels::Mono => write!(f, "1.0"), + AudioChannels::Stereo => write!(f, "2.0"), + AudioChannels::Surround51 => write!(f, "5.1"), + AudioChannels::Surround71 => write!(f, "7.1"), + AudioChannels::Unknown => write!(f, "Unknown"), + } + } +} + +// NEW: Enum for Video Dynamic Range +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum VideoDynamicRange { + #[default] + SDR, + HDR, + HDR10, + HLG, + DV, // Dolby Vision +} + +impl fmt::Display for VideoDynamicRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VideoDynamicRange::SDR => write!(f, "SDR"), + VideoDynamicRange::HDR => write!(f, "HDR"), + VideoDynamicRange::HDR10 => write!(f, "HDR10"), + VideoDynamicRange::HLG => write!(f, "HLG"), + VideoDynamicRange::DV => write!(f, "DV"), + } + } +} + +/// A struct that holds all classified media quality features. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MediaQuality { + pub resolution: VideoResolution, + pub video_codec: VideoCodec, + pub dynamic_range: VideoDynamicRange, + pub audio_codec: AudioCodec, + pub audio_channels: AudioChannels, +} + +impl MediaQuality { + /// Formats the quality features into a string suitable for filenames, e.g., "1080p | x265 | DTS | 5.1". + /// Returns an empty string if no relevant features are available to display. + pub fn format_for_filename(&self, separator: &str) -> String { + let mut parts = Vec::new(); + + if self.resolution != VideoResolution::Unknown { + parts.push(self.resolution.to_string()); + } + if self.video_codec != VideoCodec::Other { + parts.push(self.video_codec.to_string()); + } + // Only show dynamic range if it's not standard SDR. + if self.dynamic_range != VideoDynamicRange::SDR { + parts.push(self.dynamic_range.to_string()); + } + if self.audio_codec != AudioCodec::Other { + parts.push(self.audio_codec.to_string()); + } + if self.audio_channels != AudioChannels::Unknown { + parts.push(self.audio_channels.to_string()); + } + + parts.join(separator) + } + + /// Extracts media quality information from an `ffprobe` info block. + /// The `info_block` is expected to be a `serde_json::Value` object. + pub fn from_ffprobe_info(info_block: &Value) -> Option { + let video_info = info_block.get("video")?.as_object()?; + // Assuming the first audio stream is the primary one. + let audio_info = info_block.get("audio")?.as_object()?; + + // Helper to get a value by trying a prioritized list of field names. + let get_value = |obj: &Map, fields: &[&str]| -> Option { + for field in fields { + if let Some(value) = obj.get(*field) { + if !value.is_null() { + return Some(value.clone()); + } + } + } + None + }; + + // 1. Classify video resolution from width + let resolution = get_value(video_info, &["height", "coded_height"]) + .and_then(|v| v.as_u64()) + .map_or(VideoResolution::default(), |h| match h { + _ if h >= 4300 => VideoResolution::P4320, + _ if h >= 2100 => VideoResolution::P2160, + _ if h >= 1000 => VideoResolution::P1080, + _ if h >= 700 => VideoResolution::P720, + _ => VideoResolution::SD, + }); + + // 2. Classify video codec + let video_codec = get_value(video_info, &["codec_name"]) + .and_then(|v| v.as_str().map(str::to_lowercase)) + .map_or(VideoCodec::default(), |name| match name.as_str() { + "h264" => VideoCodec::H264, + "hevc" => VideoCodec::H265, + "mpeg4" => VideoCodec::MPEG4, + "vc1" => VideoCodec::VC1, + _ => VideoCodec::default(), + }); + + // 3. Classify dynamic range + let dynamic_range = { + let tag_string = get_value(video_info, &["codec_tag_string"]) + .and_then(|v| v.as_str().map(str::to_lowercase)); + + if tag_string == Some("dovi".to_string()) { + VideoDynamicRange::DV + } else { + get_value(video_info, &["color_transfer"]) + .and_then(|v| v.as_str().map(str::to_lowercase)) + .map_or(VideoDynamicRange::SDR, |ct| match ct.as_str() { + "smpte2084" => VideoDynamicRange::HDR10, + "arib-std-b67" => VideoDynamicRange::HLG, + _ => VideoDynamicRange::SDR, + }) + } + }; + + // 4. Classify audio codec + let audio_codec = get_value(audio_info, &["codec_name"]) + .and_then(|v| v.as_str().map(str::to_lowercase)) + .map_or(AudioCodec::default(), |name| match name.as_str() { + "aac" => AudioCodec::AAC, + "ac3" => AudioCodec::AC3, + "eac3" => AudioCodec::EAC3, + "dts" => AudioCodec::DTS, + "truehd" => AudioCodec::TrueHD, + "flac" => AudioCodec::FLAC, + _ => AudioCodec::default(), + }); + + // 5. Classify audio channels + let audio_channels = get_value(audio_info, &["channel_layout"]) + .and_then(|v| v.as_str().map(str::to_lowercase)) + .map_or(AudioChannels::default(), |layout| match layout.as_str() { + l if l.starts_with("7.1") => AudioChannels::Surround71, + l if l.starts_with("5.1") => AudioChannels::Surround51, + "stereo" => AudioChannels::Stereo, + "mono" => AudioChannels::Mono, + _ => AudioChannels::default(), + }); + + Some(Self { + resolution, + video_codec, + dynamic_range, + audio_codec, + audio_channels, + }) + } +} \ No newline at end of file diff --git a/backend/src/model/mod.rs b/backend/src/model/mod.rs index 158171a36..2bfd21fa6 100644 --- a/backend/src/model/mod.rs +++ b/backend/src/model/mod.rs @@ -7,6 +7,7 @@ mod healthcheck; mod playlist_categories; mod config; mod input_source; +pub mod media_properties; pub use self::playlist::*; pub use self::mapping::*; @@ -18,3 +19,4 @@ pub use self::playlist_categories::*; pub use shared::model::xtream_const::*; pub use self::config::*; pub use self::input_source::*; +pub use self::media_properties::*; diff --git a/backend/src/model/xtream.rs b/backend/src/model/xtream.rs index 6e0cd4be2..61191fa06 100644 --- a/backend/src/model/xtream.rs +++ b/backend/src/model/xtream.rs @@ -423,6 +423,14 @@ impl XtreamSeriesInfoEpisode { add_i64_property_if_exists!(result, self.episode_num, "episode"); let series_tmdb_id = info.and_then(|i| i.tmdb_id.or(i.tmdb)); add_opt_i64_property_if_exists!(result, self.info.as_ref().and_then(|info| info.tmdb_id.or(info.tmdb.or(series_tmdb_id))), "tmdb_id"); + + // Add the "info" section to the playlist item additional properties. + if let Some(episode_info) = &self.info { + if let Ok(info_value) = serde_json::to_value(episode_info) { + result.insert("info".to_string(), info_value); + } + } + if result.is_empty() { None } else { Some(Value::Object(result)) } } } diff --git a/backend/src/processing/processor/xtream_vod.rs b/backend/src/processing/processor/xtream_vod.rs index c7010fc47..df779515b 100644 --- a/backend/src/processing/processor/xtream_vod.rs +++ b/backend/src/processing/processor/xtream_vod.rs @@ -149,7 +149,14 @@ pub async fn playlist_resolve_vod(app_config: &AppConfig, client: Arc>(&content).ok().and_then(|info_doc| info_doc.get("info").cloned()); + // Add the "info" section to the playlist item additional properties. + pli.header.additional_properties = from_str::>(&content).ok().and_then(|info_doc| { + info_doc.get("info").cloned().map(|info_content| { + let mut wrapped_info = Map::new(); + wrapped_info.insert("info".to_string(), info_content); + Value::Object(wrapped_info) + }) + }); } } } diff --git a/backend/src/repository/strm_repository.rs b/backend/src/repository/strm_repository.rs index d1ce0bd59..372c22dc0 100644 --- a/backend/src/repository/strm_repository.rs +++ b/backend/src/repository/strm_repository.rs @@ -21,6 +21,8 @@ use tokio::fs::{create_dir_all, remove_dir, remove_file, File}; use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}; use shared::model::{ClusterFlags, FieldGetAccessor, PlaylistGroup, PlaylistItem, PlaylistItemType, StrmExportStyle, UUIDType}; use crate::utils; +// Import the new MediaQuality struct +use crate::model::{MediaQuality}; /// Sanitizes a string to be safe for use as a file or directory name by /// following a strict "allow-list" approach and discarding invalid characters. @@ -38,7 +40,7 @@ fn sanitize_for_filename(text: &str, underscore_whitespace: bool) -> String { // Decide which characters to keep or transform. if c.is_alphanumeric() { Some(c) - } else if "+=,._-@#()".contains(c) { // <-- Allow list of safe punctuation. + } else if "+=,._-@#()[]".contains(c) { // <-- Allow list of safe punctuation, added [ and ] for quality tags. Some(c) } else if c.is_whitespace() { if underscore_whitespace { @@ -444,7 +446,7 @@ fn format_for_kodi( let base_name = format!("{sanitized_title}{year_string}"); let folder_name = format!("{base_name}{id_string}"); - let final_filename = base_name; // Kodi is flexible, filename without ID is clean. The folder has the ID. + let final_filename = base_name; if flat { dir_path.push(format!("{folder_name}{separator}[{category}]")); @@ -506,7 +508,7 @@ fn format_for_plex( let base_name = format!("{sanitized_title}{year_string}"); let folder_name = format!("{base_name}{id_string}"); - let final_filename = base_name; // Plex: ID is only in the folder name. + let final_filename = base_name; if flat { dir_path.push(format!("{folder_name}{separator}[{category}]")); @@ -634,7 +636,7 @@ fn format_for_jellyfin( let base_name = format!("{sanitized_title}{year_string}"); let folder_name = format!("{base_name}{id_string}"); - let final_filename = base_name; // Jellyfin: ID is only in the folder name. + let final_filename = base_name; if flat { dir_path.push(format!("{folder_name}{separator}[{category}]")); @@ -676,7 +678,6 @@ fn format_for_jellyfin( } } - /// Generates style-compliant directory and file names by dispatching /// the call to a dedicated formatting function for the respective style. async fn style_based_rename( @@ -716,9 +717,7 @@ async fn prepare_strm_files( cfg: &AppConfig, new_playlist: &mut [PlaylistGroup], _root_path: &Path, - underscore_whitespace: bool, - style: &StrmExportStyle, - flat: bool, + strm_target_output: &StrmTargetOutput, ) -> Vec { let channel_count = new_playlist .iter() @@ -740,12 +739,34 @@ async fn prepare_strm_files( cfg, &strm_item_info, &mut input_tmdb_indexes, - style, - underscore_whitespace, - flat, + &strm_target_output.style, + strm_target_output.underscore_whitespace, + strm_target_output.flat, ).await; - let filename = Arc::new(strm_file_name); + // Conditionally generate the quality string based on the new config flag + let separator = if strm_target_output.underscore_whitespace { "_" } else { " " }; + let quality_string = if strm_target_output.add_quality_to_filename { + pli.header.additional_properties + .as_ref() + .and_then(|props| props.get("info")) + .and_then(MediaQuality::from_ffprobe_info) + .map_or_else(String::new, |quality| { + let formatted = quality.format_for_filename(separator); + if formatted.is_empty() { + String::new() + } else { + // Hard-coded separator for filename clarity. + format!(" - [{formatted}]") + } + }) + } else { + String::new() + }; + + let final_filename = format!("{strm_file_name}{quality_string}"); + let filename = Arc::new(final_filename); + if all_filenames.contains(&filename) { collisions.insert(Arc::clone(&filename)); } @@ -763,22 +784,19 @@ async fn prepare_strm_files( // According to the docs (Plex, Jellyfin), this should be " - " (space-hyphen-space). // The user's `underscore_whitespace` setting should not apply to this structural separator. let version_separator = " - "; - let separator = if underscore_whitespace { "_" } else { " " }; + let separator = if strm_target_output.underscore_whitespace { "_" } else { " " }; result .iter_mut() .filter(|s| collisions.contains(&s.file_name)) .for_each(|s| { // Create a descriptive and unique identifier for this version. - // This will become the "ArbitraryText" or "Label". let version_label = format!("Version{}id#{}", separator, s.strm_info.virtual_id); // The base filename is the part that is identical for all versions. let base_filename = &s.file_name; // Apply the specific multi-version naming convention for the selected style. - // NOTE: For multi-versioning, all four platforms use the same " - Suffix" logic. - // A style-specific match is not strictly necessary here but is good practice for future flexibility. - let new_filename = match style { + let new_filename = match strm_target_output.style { // Plex, Emby, and Kodi all follow the `Filename - Suffix` pattern. StrmExportStyle::Plex | StrmExportStyle::Emby | StrmExportStyle::Kodi => { format!("{base_filename}{version_separator}{version_label}") @@ -837,19 +855,11 @@ pub async fn write_strm_playlist( let target_force_redirect = target.options.as_ref().and_then(|o| o.force_redirect.as_ref()); - // we need to consider - // - Live streams - // - Xtream Series Episode (has series_name and release_date) - // - Xtream VOD (should have year or release_date) - // - M3u Series (TODO we dont have this currently, should be guessed through m3u parser) - // - M3u Vod (no additional infos, need to extract from title) let strm_files = prepare_strm_files( app_config, new_playlist, &root_path, - target_output.underscore_whitespace, - &target_output.style, - target_output.flat, + target_output, ).await; for strm_file in strm_files { // file paths diff --git a/docker/Dockerfile b/docker/Dockerfile index 85c75f43c..005380d97 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,86 +1,83 @@ # ================================================================= # -# Part 1: Prerequisite Build Stages -# -# These stages are used as building blocks for the final images. -# They prepare the Rust binary and other resources. +# Dockerfile für LOKALE Builds (nativ, ohne Cross-Compilation) # # ================================================================= # ----------------------------------------------------------------- # Stage 1: Build the Rust binary for production # ----------------------------------------------------------------- -FROM ghcr.io/cross-rs/x86_64-unknown-linux-musl:main AS rust-build +FROM rust:bookworm AS rust-build -# Get target architecture -ARG RUST_TARGET +# Get target architecture from build argument +ARG RUST_TARGET=x86_64-unknown-linux-musl -RUN apt-get update && apt-get install -y --no-install-recommends pkg-config musl-tools libssl-dev +# Install build dependencies for Debian (bookworm) +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + pkg-config libssl-dev musl-tools && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* -# Update Rust toolchain and add necessary target -RUN rustup update && rustup target add $RUST_TARGET +# Set Rust compiler flags for static linking against musl +ENV RUSTFLAGS='-C target-feature=+crt-static' -# Set Rust compiler flags for better optimization and reproducibility -ENV RUSTFLAGS='--remap-path-prefix $HOME=~ -C target-feature=+crt-static' - -# Copy dependency files first for better layer caching +# Copy dependency files and the full 'shared' crate for dependency caching WORKDIR /src COPY Cargo.toml Cargo.lock ./ +COPY shared ./shared/ COPY backend/Cargo.toml ./backend/ COPY frontend/Cargo.toml ./frontend/ -COPY shared/Cargo.toml ./shared/ -# Create dummy source files to build dependencies only -RUN mkdir -p src backend/src frontend/src shared/src && \ - echo "fn main() {}" > src/main.rs && \ +# Create dummy main.rs for other crates to allow dependency build +RUN mkdir -p backend/src frontend/src && \ echo "fn main() {}" > backend/src/main.rs && \ - echo "fn main() {}" > frontend/src/main.rs && \ - echo "pub fn dummy() {}" > shared/src/lib.rs + echo "fn main() {}" > frontend/src/main.rs -# Build dependencies (this layer will be cached unless dependencies change) -RUN cargo build -p tuliprox --target $RUST_TARGET --release || true -RUN cargo build -p shared --target $RUST_TARGET --release || true +# Build dependencies (this layer will be cached unless dependencies or 'shared' change) +RUN rustup target add ${RUST_TARGET} +RUN cargo build -p tuliprox --target ${RUST_TARGET} --release || true -# Now copy the actual source code and build the project -COPY . . - -RUN cargo build -p tuliprox --target $RUST_TARGET --release +# Now copy the rest of the source code and build the project +COPY backend ./backend/ +COPY frontend ./frontend/ +RUN cargo build -p tuliprox --target ${RUST_TARGET} --release # ----------------------------------------------------------------- # Stage 2: Build the rust frontend # ----------------------------------------------------------------- -FROM ghcr.io/cross-rs/x86_64-unknown-linux-musl:main AS trunk-build - -ARG RUST_TARGET=wasm32-unknown-unknown - -# Set working directory -WORKDIR /src +FROM rust:bookworm AS trunk-build # Install dependencies for Trunk and WebAssembly -RUN apt-get update && apt-get install -y --no-install-recommends \ - pkg-config libssl-dev curl libclang-dev binaryen +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + pkg-config libssl-dev curl binaryen && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* # Add wasm target & install trunk RUN rustup target add wasm32-unknown-unknown RUN cargo install --locked trunk wasm-bindgen-cli -# Copy dependency files first for better layer caching -COPY Cargo.toml Cargo.lock ./ -COPY frontend/Cargo.toml frontend/Trunk.toml ./frontend/ -COPY shared/Cargo.toml ./shared/ +# Set working directory +WORKDIR /src -# Create dummy source files -RUN mkdir -p frontend/src shared/src && \ +# Copy dependency files and the full 'shared' crate for dependency caching +COPY Cargo.toml Cargo.lock ./ +COPY shared ./shared/ +COPY backend/Cargo.toml ./backend/ +COPY frontend/Cargo.toml frontend/Trunk.toml ./frontend/ + +# Create dummy main.rs for BOTH frontend AND backend to satisfy workspace requirements +RUN mkdir -p frontend/src backend/src && \ echo "fn main() {}" > frontend/src/main.rs && \ - echo "pub fn dummy() {}" > shared/src/lib.rs + echo "fn main() {}" > backend/src/main.rs # Pre-build dependencies WORKDIR /src/frontend RUN cargo build --target wasm32-unknown-unknown --release || true -# Now copy the actual source code and build the project +# Now copy the rest of the source code and build the project WORKDIR /src -COPY . . +COPY frontend ./frontend/ WORKDIR /src/frontend @@ -132,9 +129,6 @@ RUN apk add --no-cache tzdata \ # # Part 2: Final Image Stages # -# These stages build the final, runnable images by assembling -# the artifacts from the prerequisite stages above. -# # ================================================================= # ----------------------------------------------------------------- @@ -142,7 +136,7 @@ RUN apk add --no-cache tzdata \ # ----------------------------------------------------------------- FROM rust:alpine AS debug -# For Alpine, the correct target architecture uses 'musl' +# Make the build-time argument available as a run-time environment variable ARG RUST_TARGET=x86_64-unknown-linux-musl ARG TZ=UTC @@ -150,14 +144,15 @@ ARG TZ=UTC ENV RUST_TARGET=${RUST_TARGET} ENV TZ=${TZ} -# Install build dependencies and the lldb debugger for Alpine -# Added 'perl' which is required to build the native OpenSSL library. -RUN apk add --no-cache bash build-base openssl-dev lldb gdb perl +# Install build dependencies and debugger for Alpine. +# 'build-base' contains gcc, make, etc. +# 'gdb' is required for the debugger (lldb is often more complicated in Alpine repos) +RUN apk add --no-cache bash build-base openssl-dev perl lldb gdb # Update Rust toolchain and add the necessary target RUN rustup update && rustup target add $RUST_TARGET -# Create the final application layout to mimic production +# Create the app layout to simulate the production environment WORKDIR /app COPY --from=trunk-build /src/frontend/dist ./web COPY --from=resource-build /src/resources ./resources @@ -166,13 +161,15 @@ COPY --from=resource-build /src/resources ./resources WORKDIR /usr/src/tuliprox COPY . . -# Copy the entrypoint script and set it as the entrypoint for the container. +# Copy the entrypoint script and set it as the entry point for the container COPY ./docker/debug/debug-entrypoint.sh /usr/local/bin/entrypoint.sh + # Ensure the script is executable RUN chmod +x /usr/local/bin/entrypoint.sh ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] -# The CMD will be passed as arguments to the entrypoint script. +# The CMD is passed as an argument to the Entrypoint script. +# This keeps the container running indefinitely for interactive debugging. CMD ["tail", "-f", "/dev/null"] # ----------------------------------------------------------------- @@ -180,17 +177,15 @@ CMD ["tail", "-f", "/dev/null"] # ----------------------------------------------------------------- FROM scratch AS scratch-final -ARG RUST_TARGET +ARG RUST_TARGET=x86_64-unknown-linux-musl # Copy timezone data and localtime from tz-prep COPY --from=tz-prep /output/usr/share/zoneinfo /usr/share/zoneinfo COPY --from=tz-prep /output/etc/localtime /etc/localtime -# Certificates +# Copy SSL certificates from a builder stage that has them COPY --from=rust-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -# RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime - # App WORKDIR /app COPY --from=rust-build /src/target/${RUST_TARGET}/release/tuliprox ./tuliprox @@ -205,14 +200,18 @@ CMD ["-s", "-p", "/app/config"] # ----------------------------------------------------------------- FROM alpine:latest AS alpine-final -ARG RUST_TARGET +ARG RUST_TARGET=x86_64-unknown-linux-musl ARG TZ=UTC ENV TZ=${TZ} RUN apk add --no-cache bash curl ca-certificates tini -COPY --from=rust-build /usr/share/zoneinfo /usr/share/zoneinfo -COPY --from=rust-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +# Copy timezone data and localtime from tz-prep +COPY --from=tz-prep /output/usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=tz-prep /output/etc/localtime /etc/localtime + +# Copy SSL certificates `ca-certificates` from Alpine-based Stage (tz-prep ist Alpine) +COPY --from=tz-prep /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ # Default timezone fallback RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime @@ -223,4 +222,4 @@ COPY --from=trunk-build /src/frontend/dist ./web COPY --from=resource-build /src/resources ./resources ENTRYPOINT ["/sbin/tini", "--", "/app/tuliprox"] -CMD ["-s", "-p", "/app/config"] +CMD ["-s", "-p", "/app/config"] \ No newline at end of file diff --git a/docker/Dockerfile-cross b/docker/Dockerfile-cross new file mode 100644 index 000000000..85c75f43c --- /dev/null +++ b/docker/Dockerfile-cross @@ -0,0 +1,226 @@ +# ================================================================= +# +# Part 1: Prerequisite Build Stages +# +# These stages are used as building blocks for the final images. +# They prepare the Rust binary and other resources. +# +# ================================================================= + +# ----------------------------------------------------------------- +# Stage 1: Build the Rust binary for production +# ----------------------------------------------------------------- +FROM ghcr.io/cross-rs/x86_64-unknown-linux-musl:main AS rust-build + +# Get target architecture +ARG RUST_TARGET + +RUN apt-get update && apt-get install -y --no-install-recommends pkg-config musl-tools libssl-dev + +# Update Rust toolchain and add necessary target +RUN rustup update && rustup target add $RUST_TARGET + +# Set Rust compiler flags for better optimization and reproducibility +ENV RUSTFLAGS='--remap-path-prefix $HOME=~ -C target-feature=+crt-static' + +# Copy dependency files first for better layer caching +WORKDIR /src +COPY Cargo.toml Cargo.lock ./ +COPY backend/Cargo.toml ./backend/ +COPY frontend/Cargo.toml ./frontend/ +COPY shared/Cargo.toml ./shared/ + +# Create dummy source files to build dependencies only +RUN mkdir -p src backend/src frontend/src shared/src && \ + echo "fn main() {}" > src/main.rs && \ + echo "fn main() {}" > backend/src/main.rs && \ + echo "fn main() {}" > frontend/src/main.rs && \ + echo "pub fn dummy() {}" > shared/src/lib.rs + +# Build dependencies (this layer will be cached unless dependencies change) +RUN cargo build -p tuliprox --target $RUST_TARGET --release || true +RUN cargo build -p shared --target $RUST_TARGET --release || true + +# Now copy the actual source code and build the project +COPY . . + +RUN cargo build -p tuliprox --target $RUST_TARGET --release + +# ----------------------------------------------------------------- +# Stage 2: Build the rust frontend +# ----------------------------------------------------------------- +FROM ghcr.io/cross-rs/x86_64-unknown-linux-musl:main AS trunk-build + +ARG RUST_TARGET=wasm32-unknown-unknown + +# Set working directory +WORKDIR /src + +# Install dependencies for Trunk and WebAssembly +RUN apt-get update && apt-get install -y --no-install-recommends \ + pkg-config libssl-dev curl libclang-dev binaryen + +# Add wasm target & install trunk +RUN rustup target add wasm32-unknown-unknown +RUN cargo install --locked trunk wasm-bindgen-cli + +# Copy dependency files first for better layer caching +COPY Cargo.toml Cargo.lock ./ +COPY frontend/Cargo.toml frontend/Trunk.toml ./frontend/ +COPY shared/Cargo.toml ./shared/ + +# Create dummy source files +RUN mkdir -p frontend/src shared/src && \ + echo "fn main() {}" > frontend/src/main.rs && \ + echo "pub fn dummy() {}" > shared/src/lib.rs + +# Pre-build dependencies +WORKDIR /src/frontend +RUN cargo build --target wasm32-unknown-unknown --release || true + +# Now copy the actual source code and build the project +WORKDIR /src +COPY . . + +WORKDIR /src/frontend + +# Run Trunk build +RUN trunk build --release + +# ----------------------------------------------------------------- +# Stage 3: Build video resources with ffmpeg +# ----------------------------------------------------------------- +FROM linuxserver/ffmpeg:latest AS resource-build + +WORKDIR /src +COPY resources ./resources + +# Combine ffmpeg commands into a single layer to reduce image size +RUN ffmpeg -loop 1 -i ./resources/channel_unavailable.jpg -t 10 -r 1 -an \ + -vf "scale=1920:1080" \ + -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p \ + ./resources/channel_unavailable.ts && \ + ffmpeg -loop 1 -i ./resources/user_connections_exhausted.jpg -t 10 -r 1 -an \ + -vf "scale=1920:1080" \ + -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p \ + ./resources/user_connections_exhausted.ts && \ + ffmpeg -loop 1 -i ./resources/provider_connections_exhausted.jpg -t 10 -r 1 -an \ + -vf "scale=1920:1080" \ + -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p \ + ./resources/provider_connections_exhausted.ts && \ + ffmpeg -loop 1 -i ./resources/user_account_expired.jpg -t 10 -r 1 -an \ + -vf "scale=1920:1080" \ + -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p \ + ./resources/user_account_expired.ts + +# ----------------------------------------------------------------- +# Stage 4: Prepare timezone data +# ----------------------------------------------------------------- +FROM alpine:latest AS tz-prep + +ARG TZ=UTC +ENV TZ=${TZ} + +RUN apk add --no-cache tzdata \ + && mkdir -p /output/etc \ + && mkdir -p /output/usr/share \ + && cp -r /usr/share/zoneinfo /output/usr/share/zoneinfo \ + && ln -sf /usr/share/zoneinfo/${TZ} /output/etc/localtime + + +# ================================================================= +# +# Part 2: Final Image Stages +# +# These stages build the final, runnable images by assembling +# the artifacts from the prerequisite stages above. +# +# ================================================================= + +# ----------------------------------------------------------------- +# Final Image #1: Debugging Environment (Alpine-based) +# ----------------------------------------------------------------- +FROM rust:alpine AS debug + +# For Alpine, the correct target architecture uses 'musl' +ARG RUST_TARGET=x86_64-unknown-linux-musl +ARG TZ=UTC + +# Make the build-time argument available as a run-time environment variable +ENV RUST_TARGET=${RUST_TARGET} +ENV TZ=${TZ} + +# Install build dependencies and the lldb debugger for Alpine +# Added 'perl' which is required to build the native OpenSSL library. +RUN apk add --no-cache bash build-base openssl-dev lldb gdb perl + +# Update Rust toolchain and add the necessary target +RUN rustup update && rustup target add $RUST_TARGET + +# Create the final application layout to mimic production +WORKDIR /app +COPY --from=trunk-build /src/frontend/dist ./web +COPY --from=resource-build /src/resources ./resources + +# Create a separate directory for the source code +WORKDIR /usr/src/tuliprox +COPY . . + +# Copy the entrypoint script and set it as the entrypoint for the container. +COPY ./docker/debug/debug-entrypoint.sh /usr/local/bin/entrypoint.sh +# Ensure the script is executable +RUN chmod +x /usr/local/bin/entrypoint.sh +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + +# The CMD will be passed as arguments to the entrypoint script. +CMD ["tail", "-f", "/dev/null"] + +# ----------------------------------------------------------------- +# Final Image #2: Production image based on scratch +# ----------------------------------------------------------------- +FROM scratch AS scratch-final + +ARG RUST_TARGET + +# Copy timezone data and localtime from tz-prep +COPY --from=tz-prep /output/usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=tz-prep /output/etc/localtime /etc/localtime + +# Certificates +COPY --from=rust-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime + +# App +WORKDIR /app +COPY --from=rust-build /src/target/${RUST_TARGET}/release/tuliprox ./tuliprox +COPY --from=trunk-build /src/frontend/dist ./web +COPY --from=resource-build /src/resources ./resources + +ENTRYPOINT ["/app/tuliprox"] +CMD ["-s", "-p", "/app/config"] + +# ----------------------------------------------------------------- +# Final Image #3: Production image based on Alpine +# ----------------------------------------------------------------- +FROM alpine:latest AS alpine-final + +ARG RUST_TARGET +ARG TZ=UTC +ENV TZ=${TZ} + +RUN apk add --no-cache bash curl ca-certificates tini + +COPY --from=rust-build /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=rust-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Default timezone fallback +RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime + +WORKDIR /app +COPY --from=rust-build /src/target/${RUST_TARGET}/release/tuliprox ./tuliprox +COPY --from=trunk-build /src/frontend/dist ./web +COPY --from=resource-build /src/resources ./resources + +ENTRYPOINT ["/sbin/tini", "--", "/app/tuliprox"] +CMD ["-s", "-p", "/app/config"] diff --git a/docker/debug/.vscode/launch.json b/docker/debug/.vscode/launch.json index 7d37af129..bd3933330 100644 --- a/docker/debug/.vscode/launch.json +++ b/docker/debug/.vscode/launch.json @@ -3,33 +3,39 @@ "configurations": [ { - "name": "Docker Remote Debug (start & attach)", + "name": "Docker Remote Debug (LLDB, start & attach)", "type": "lldb", "request": "launch", + // 0. Build and Recreate debug container stack + "preLaunchTask": "docker-compose-up-debug", + "initCommands": [ "platform select remote-linux", "platform connect connect://localhost:10586" ], + // 1. Load the local binary to load symbols and source code information. "targetCreateCommands": [ - "target create /usr/src/tuliprox/target/x86_64-unknown-linux-musl/debug/tuliprox" + // Opt#1: binary path inside the docker container, for musl target (alpine-based image) + "target create ${workspaceFolder}/debug/target/x86_64-unknown-linux-musl/debug/tuliprox" + + // Opt#2: binary path inside the docker container, for gnu target (debian-based image) + // "target create ${workspaceFolder}/debug/target/x86_64-unknown-linux-gnu/debug/tuliprox" ], + // 2. Connect to the remote debugger and continue the process. "processCreateCommands": [ - // Attache to the process and continue after attaching "attach -n tuliprox", "continue" ], "sourceMap": { "/usr/src/tuliprox": "${workspaceFolder}" - }, - - "preLaunchTask": "docker-compose-up-debug" + } }, { - "name": "Docker Remote Debug (attach only)", + "name": "Docker Remote Debug (LLDB, attach only)", "type": "lldb", "request": "launch", @@ -39,11 +45,10 @@ ], "targetCreateCommands": [ - "target create /usr/src/tuliprox/target/x86_64-unknown-linux-musl/debug/tuliprox" + "target create ${workspaceFolder}/debug/target/x86_64-unknown-linux-musl/debug/tuliprox" ], "processCreateCommands": [ - // Attache to the process and continue after attaching "attach -n tuliprox", "continue" ], diff --git a/docker/debug/.vscode/tasks.json b/docker/debug/.vscode/tasks.json index 41d920797..15389358e 100644 --- a/docker/debug/.vscode/tasks.json +++ b/docker/debug/.vscode/tasks.json @@ -4,7 +4,12 @@ { "label": "docker-compose-up-debug", "type": "shell", + // command to build and start the Docker Compose stack for debugging "command": "docker compose -f docker-compose.yml -f docker-compose.debug-override.yml up --build --force-recreate -d --wait", + "options": { + // This tells VS Code to run the command in the parent directory of the VSCODE workspaceFolder + "cwd": "${workspaceFolder}" + }, "problemMatcher": [], "presentation": { "reveal": "always", @@ -16,6 +21,9 @@ "label": "docker-compose-down", "type": "shell", "command": "docker compose -f docker-compose.yml -f docker-compose.debug-override.yml down", + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": [], "presentation": { "reveal": "silent", diff --git a/docker/debug/HOWTO.md b/docker/debug/HOWTO.md index 5127b8ca2..c794d4fc7 100644 --- a/docker/debug/HOWTO.md +++ b/docker/debug/HOWTO.md @@ -1,73 +1,98 @@ # Remote Debugging tuliprox in VSCode with Docker -This guide explains how to set up remote debugging for the tuliprox project in VSCode using a Docker container. +This guide explains how to set up remote debugging for the tuliprox project in VSCode, using either **GDB** or **LLDB** inside a Docker container. ## Prerequisites - VSCode with the following extensions: - - [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) - - [Docker](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker) -- Docker and Docker Compose installed -- Rust toolchain (if building locally) + - **C/C++** from Microsoft (for GDB support): [Marketplace Link](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) + - **CodeLLDB** (for LLDB support): [Marketplace Link](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) + - **Docker**: [Marketplace Link](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker) +- Docker and Docker Compose installed. +- Rust toolchain (for local symbol resolution). ## Setup -1. Copy the debug configuration files to your project root: - ```bash - cp -r docker/debug/.vscode . - cp docker/debug/docker-compose.debug-override.yml . - ``` +1. Copy the debug configuration files to your project's root directory: + ```bash + # Copy VSCode tasks and launch configurations + cp -r docker/debug/.vscode . + + # Copy the Docker Compose override file for debugging + cp docker/debug/docker-compose.debug-override.yml . + ``` ## Configuration -### Docker Compose -The `docker-compose.debug-override.yml` file: -- Configures LLDB debug ports (10586 for control, 10600-10700 for sessions) -- Sets up privileged mode for debugging -- Mounts cargo cache volumes for faster builds -- Uses the debug build target +### 1. Choose Your Debugger (`docker-compose.debug-override.yml`) -### VSCode Tasks -The `.vscode/tasks.json` defines: -- `docker-compose-up-debug`: Builds and starts containers with debug config -- `docker-compose-down`: Stops and removes containers +The `docker-compose.debug-override.yml` file is the central point for configuring your debug session. You must choose which debugger to use by setting the `DEBUG_SERVER` environment variable. -### VSCode Launch Configurations -The `.vscode/launch.json` provides two debug configurations: -1. **Docker Remote Debug (start & attach)**: - - Automatically starts containers and attaches debugger - - Uses the `docker-compose-up-debug` pre-launch task -2. **Docker Remote Debug (attach only)**: - - Only attaches debugger to running container +- Open `docker-compose.debug-override.yml`. +- Under the `environment` section for the `tuliprox` service, set `DEBUG_SERVER` to either `gdb` or `lldb`. + +**Example for using GDB:** +```yaml +services: + tuliprox: + # ... other settings + environment: + - DEBUG_SERVER=gdb + - RUST_TARGET=x86_64-unknown-linux-musl +``` + +**Example for using LLDB:** +```yaml +services: + tuliprox: + # ... other settings + environment: + - DEBUG_SERVER=lldb + - RUST_TARGET=x86_64-unknown-linux-musl +``` + +This file also: +- Exposes the debug port (default: `10586`). +- Sets up privileged mode, which is required for some debuggers. +- Mounts cargo cache volumes for faster subsequent builds. + +### 2. VSCode Launch Configurations (`.vscode/launch.json`) + +This file contains pre-configured launch profiles for LLDB (preferred debugger in VSCode). +You don't need to edit this file, just select the correct profile in VSCode. + +### 3. VSCode Tasks (`.vscode/tasks.json`) + +- **docker-compose-up-debug**: Builds and starts the container with your debug configuration. +- **docker-compose-down**: Stops and removes the container. ## Debugging Workflow -1. Open the project in VSCode -2. Set breakpoints in your Rust code -3. Select the debug configuration from the Run and Debug panel: - - Use "Docker Remote Debug (start & attach)" for a complete start-to-debug workflow - - Use "Docker Remote Debug (attach only)" if containers are already running -4. Press F5 to start debugging +1. **Configure Debugger**: Open `docker-compose.debug-override.yml` and set the `DEBUG_SERVER` variable to either `gdb` or `lldb`. +2. **Open Project**: Open the project in VSCode. +3. **Set Breakpoints**: Place breakpoints in your Rust code. +4. **Start Debugging**: + - Go to the "Run and Debug" panel (Ctrl+Shift+D). + - Select the launch configuration that matches your choice in step 1 (e.g., "Docker: GDB Remote Attach"). + - Press **F5** to start the `preLaunchTask`, which will build and run your Docker container, and then attach the debugger. ## Troubleshooting -### Common Issues +### 1. Debugger Fails to Connect or Disconnects Immediately -1. **Debugger fails to connect**: - - Verify ports 10586 and 10600-10700 are available - - Check Docker container logs for errors - - Ensure the container is running in privileged mode +- **Check `DEBUG_SERVER` variable**: Ensure the `DEBUG_SERVER` variable in `docker-compose.debug-override.yml` matches the launch configuration you selected in VSCode. +- **Check Ports**: Verify that the debug port (default `10586`) is not being used by another application on your host machine. +- **Check Container Logs**: Look at the Docker container logs. You should see output from `debug-entrypoint.sh` indicating that the debug server (GDB or LLDB) has started and is listening. -2. **Breakpoints not hitting**: - - Verify source mapping is correct in launch.json - - Ensure you're using the debug build (`target/debug/tuliprox`) +### 2. Breakpoints Are Not Being Hit -3. **Performance issues**: - - The debug build is slower than release - - Consider adding more RAM to Docker if needed +- **Verify Build Target**: Ensure the `RUST_TARGET` in your `docker-compose.debug-override.yml` matches the target you are building for. +- **Check Source Mapping**: + - For GDB (`cppdbg`), verify the `sourceFileMap` in `.vscode/launch.json`. The remote path (`/usr/src/tuliprox`) should map to your local project directory (`${workspaceFolder}`). + - For LLDB, verify the `sourceMap` setting. +- **Debug Build**: Make sure you are running a debug build, not a release build, as release builds may optimize out debug symbols. The provided scripts default to a debug build. -### Debugging Tips +### 4. Performance Issues -- Use the VSCode debug console for LLDB commands -- The debugger supports all standard LLDB features (watch, call stack, etc.) -- Breakpoints can be set while the program is running \ No newline at end of file +- The debug build is significantly slower and uses more memory than a release build. This is normal. +- Consider allocating more resources (CPU/RAM) to Docker if your application is complex. \ No newline at end of file diff --git a/docker/debug/debug-entrypoint.sh b/docker/debug/debug-entrypoint.sh index 1254ea4be..6841461f6 100755 --- a/docker/debug/debug-entrypoint.sh +++ b/docker/debug/debug-entrypoint.sh @@ -1,34 +1,63 @@ #!/bin/sh set -e # Exit immediately if a command exits with a non-zero status. -# --- LLDB Server Configuration --- -# Read port configuration from environment variables, with sane defaults. +# --- General Configuration --- +# Use DEBUG_SERVER environment variable to choose the debugger. Defaults to 'gdb'. +DEBUG_SERVER=${DEBUG_SERVER:-gdb} +# Default Rust target, can be overridden. +RUST_TARGET=${RUST_TARGET:-x86_64-unknown-linux-musl} +# Define paths and arguments once to keep it DRY. +BINARY_PATH="/usr/src/tuliprox/target/${RUST_TARGET}/debug/tuliprox" +APP_ARGS="-s -p /app/config" + +# --- Debugger Ports --- +GDB_SERVER_PORT=${GDB_SERVER_PORT:-10586} LLDB_SERVER_PORT=${LLDB_SERVER_PORT:-10586} LLDB_MIN_PORT=${LLDB_MIN_PORT:-10600} LLDB_MAX_PORT=${LLDB_MAX_PORT:-10700} -# --- Build Step --- -# 1. Ensure the Rust binary is up-to-date. +# --- Build Step (common for both) --- echo ">>> [debug-entrypoint] Ensuring the binary is up-to-date for target: ${RUST_TARGET}..." cargo build -p tuliprox --target "${RUST_TARGET}" -# --- LLDB Server Start --- -# 2. Start lldb-server in platform mode in the background. -# It listens on the main control port and uses a dedicated range for communication. -echo ">>> [debug-entrypoint] Starting lldb-server..." -echo " Control Port: ${LLDB_SERVER_PORT}" -echo " Session Ports: ${LLDB_MIN_PORT}-${LLDB_MAX_PORT}" +# --- Start Debug Server based on selection --- +case "${DEBUG_SERVER}" in + gdb) + # GDB: Launch the application *through* gdbserver. + # The app will wait for a debugger to connect before starting. + echo ">>> [debug-entrypoint] Starting GDB server..." + echo " Mode: Launching application via gdbserver" + echo " Listening on: 0.0.0.0:${GDB_SERVER_PORT}" + exec /usr/bin/gdbserver "0.0.0.0:${GDB_SERVER_PORT}" ${BINARY_PATH} ${APP_ARGS} + ;; -/usr/bin/lldb-server platform \ - --listen "*:${LLDB_SERVER_PORT}" \ - --server \ - --min-gdbserver-port ${LLDB_MIN_PORT} \ - --max-gdbserver-port ${LLDB_MAX_PORT} & + lldb) + # LLDB: Start lldb-server in the background, then start the application. + # The debugger will attach to the already running application. + echo ">>> [debug-entrypoint] Starting LLDB server..." + echo " Mode: Platform mode (attach to running process)" + echo " Control Port: ${LLDB_SERVER_PORT}" + echo " Session Ports: ${LLDB_MIN_PORT}-${LLDB_MAX_PORT}" -# --- Application Start --- -# 3. Start the actual application in the foreground. -# `exec` replaces the shell process, making the application the main process. -echo ">>> [debug-entrypoint] Starting application 'tuliprox'. Ready for debugger to attach..." -exec /usr/src/tuliprox/target/${RUST_TARGET}/debug/tuliprox -s -p /app/config \ No newline at end of file + /usr/bin/lldb-server platform \ + --listen "0.0.0.0:${LLDB_SERVER_PORT}" \ + --server \ + --min-gdbserver-port ${LLDB_MIN_PORT} \ + --max-gdbserver-port ${LLDB_MAX_PORT} & + + # Wait a moment for lldb-server to initialize before starting the app. + sleep 1 + + echo ">>> [debug-entrypoint] Starting application 'tuliprox'. Ready for debugger to attach..." + exec ${BINARY_PATH} ${APP_ARGS} + ;; + + *) + # Error case for invalid DEBUG_SERVER value. + echo "!!! [debug-entrypoint] ERROR: Invalid DEBUG_SERVER value '${DEBUG_SERVER}'." >&2 + echo " Supported values are 'gdb' or 'lldb'." >&2 + exit 1 + ;; +esac \ No newline at end of file diff --git a/docker/debug/docker-compose.debug-override.yml b/docker/debug/docker-compose.debug-override.yml index 0c704a110..b2ff5c073 100644 --- a/docker/debug/docker-compose.debug-override.yml +++ b/docker/debug/docker-compose.debug-override.yml @@ -13,19 +13,14 @@ # The value is REPLACED. # =================================================================== version: '3.8' - -# Define named volumes that will persist on the host machine -volumes: - cargo_cache: - cargo_registry: - services: gluetun_vpn_1: + # Add additional exposed ports for debug server ports: - "1080:1080" - "8901:8901" # Add debugging server for VSCode - - "10586:10586" # LLDB-Server (main control port) + - "10586:10586" # Port of GDB- or LLDB-Server (main control port) - "10600-10700:10600-10700" # LLDB-Server (range for the debug sessions) tuliprox: @@ -37,8 +32,11 @@ services: args: # Use the 'musl' target for the Alpine-based debug image RUST_TARGET: x86_64-unknown-linux-musl - # # Use the 'gnu' target for the Debian-based debug image + + # Use the 'gnu' target for the Debian-based debug image # RUST_TARGET: x86_64-unknown-linux-gnu + + # Define exposed ports on VPN network stack, if used # ports: # - "8901:8901" # # Add debugging server for VSCode @@ -46,9 +44,15 @@ services: # - "10600-10700:10600-10700" # LLDB-Server (range for the debug sessions) environment: # Pass port configuration to the entrypoint script + - DEBUG_SERVER=lldb # Use 'gdb' or 'lldb' for the debug server + # Ports for LLDB-Server - LLDB_SERVER_PORT=10586 - LLDB_MIN_PORT=10600 - LLDB_MAX_PORT=10700 + # Port for GDB-Server + - GDB_SERVER_PORT=10586 + # Specify the Rust target triple, see above in 'build.args' + - RUST_TARGET=x86_64-unknown-linux-musl # Must be set to allow process debugging privileged: true @@ -64,7 +68,6 @@ services: - "./data:/app/data:rw" - "./backup:/app/backup:rw" - "./downloads:/app/downloads:rw" - # Mounts the project's build artifacts cache - - cargo_cache:/usr/src/tuliprox/target - # Mounts the cache for downloaded crate dependencies - - cargo_registry:/usr/local/cargo/registry + # Store the project's build artifacts and cache the host for debugging and faster rebuilds + - "./debug/target:/usr/src/tuliprox/target" + - "./debug/registry:/usr/local/cargo/registry" diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 339dbb62f..728ceb520 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -294,7 +294,8 @@ "CLUSTER": "Type", "USER_AGENT": "Player", "FORMAT": "Format", - "DURATION": "Duration" + "DURATION": "Duration", + "ADD_QUALITY_TO_FILENAME": "Quality" }, "TITLE": { "USER_BOUQUET_EDITOR": "User group editor" diff --git a/frontend/src/app/components/playlist/target/strm_output.rs b/frontend/src/app/components/playlist/target/strm_output.rs index cf614bbe2..6a6a42991 100644 --- a/frontend/src/app/components/playlist/target/strm_output.rs +++ b/frontend/src/app/components/playlist/target/strm_output.rs @@ -22,6 +22,7 @@ pub fn StrmOutput(props: &StrmOutputProps) -> Html { Rc::new(Tag { class: convert_bool_to_chip_style(output.flat), label: translate.t("LABEL.FLAT") }), Rc::new(Tag { class: convert_bool_to_chip_style(output.cleanup), label: translate.t("LABEL.CLEANUP") }), Rc::new(Tag { class: convert_bool_to_chip_style(output.underscore_whitespace), label: translate.t("LABEL.UNDERSCORE_WHITESPACE") }), + Rc::new(Tag { class: convert_bool_to_chip_style(output.add_quality_to_filename), label: translate.t("LABEL.ADD_QUALITY_TO_FILENAME") }), ] }) }; diff --git a/shared/src/model/config/target.rs b/shared/src/model/config/target.rs index 932494c55..5d01a48f7 100644 --- a/shared/src/model/config/target.rs +++ b/shared/src/model/config/target.rs @@ -122,6 +122,8 @@ pub struct StrmTargetOutputDto { pub filter: Option, #[serde(skip)] pub t_filter: Option, + #[serde(default)] + pub add_quality_to_filename: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)]