treewide: fix various UI bugs; optimize crypto dependencies & format

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
This commit is contained in:
raf 2026-02-10 12:56:05 +03:00
commit 3ccddce7fd
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
178 changed files with 58285 additions and 54241 deletions

View file

@ -1,240 +1,269 @@
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::{
extract::{Path, State},
http::StatusCode,
};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use pinakes_core::{
model::MediaId,
transcode::{estimate_bandwidth, parse_resolution},
};
use uuid::Uuid;
use crate::error::ApiError;
use crate::state::AppState;
use pinakes_core::model::MediaId;
use pinakes_core::transcode::{estimate_bandwidth, parse_resolution};
use crate::{error::ApiError, state::AppState};
fn escape_xml(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
pub async fn hls_master_playlist(
State(state): State<AppState>,
Path(id): Path<Uuid>,
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<axum::response::Response, ApiError> {
// Verify media exists
let _item = state.storage.get_media(MediaId(id)).await?;
// Verify media exists
let _item = state.storage.get_media(MediaId(id)).await?;
let config = state.config.read().await;
let profiles = &config.transcoding.profiles;
let config = state.config.read().await;
let profiles = &config.transcoding.profiles;
let mut playlist = String::from("#EXTM3U\n#EXT-X-VERSION:3\n\n");
let mut playlist = String::from("#EXTM3U\n#EXT-X-VERSION:3\n\n");
for profile in profiles {
let (w, h) = parse_resolution(&profile.max_resolution);
let bandwidth = estimate_bandwidth(profile);
let encoded_name = utf8_percent_encode(&profile.name, NON_ALPHANUMERIC).to_string();
playlist.push_str(&format!(
"#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},RESOLUTION={w}x{h}\n\
/api/v1/media/{id}/stream/hls/{encoded_name}/playlist.m3u8\n\n",
));
}
for profile in profiles {
let (w, h) = parse_resolution(&profile.max_resolution);
let bandwidth = estimate_bandwidth(profile);
let encoded_name =
utf8_percent_encode(&profile.name, NON_ALPHANUMERIC).to_string();
playlist.push_str(&format!(
"#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},RESOLUTION={w}x{h}\n/api/v1/\
media/{id}/stream/hls/{encoded_name}/playlist.m3u8\n\n",
));
}
Ok(axum::response::Response::builder()
.header("Content-Type", "application/vnd.apple.mpegurl")
.body(axum::body::Body::from(playlist))
.unwrap())
Ok(
axum::response::Response::builder()
.header("Content-Type", "application/vnd.apple.mpegurl")
.body(axum::body::Body::from(playlist))
.unwrap(),
)
}
pub async fn hls_variant_playlist(
State(state): State<AppState>,
Path((id, profile)): Path<(Uuid, String)>,
State(state): State<AppState>,
Path((id, profile)): Path<(Uuid, String)>,
) -> Result<axum::response::Response, ApiError> {
let item = state.storage.get_media(MediaId(id)).await?;
let duration = item.duration_secs.unwrap_or(0.0);
if duration <= 0.0 {
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"cannot generate HLS playlist for media with unknown or zero duration".into(),
),
));
}
let segment_duration = 10.0;
let num_segments = (duration / segment_duration).ceil() as usize;
let item = state.storage.get_media(MediaId(id)).await?;
let duration = item.duration_secs.unwrap_or(0.0);
if duration <= 0.0 {
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"cannot generate HLS playlist for media with unknown or zero duration"
.into(),
),
));
}
let segment_duration = 10.0;
let num_segments = (duration / segment_duration).ceil() as usize;
let mut playlist = String::from(
"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:10\n#EXT-X-MEDIA-SEQUENCE:0\n",
);
for i in 0..num_segments.max(1) {
let seg_dur = if i == num_segments.saturating_sub(1) && duration > 0.0 {
duration - (i as f64 * segment_duration)
} else {
segment_duration
};
playlist.push_str(&format!("#EXTINF:{seg_dur:.3},\n"));
playlist.push_str(&format!(
"/api/v1/media/{id}/stream/hls/{profile}/segment{i}.ts\n"
));
}
playlist.push_str("#EXT-X-ENDLIST\n");
let mut playlist = String::from(
"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:10\n#\
EXT-X-MEDIA-SEQUENCE:0\n",
);
for i in 0..num_segments.max(1) {
let seg_dur = if i == num_segments.saturating_sub(1) && duration > 0.0 {
duration - (i as f64 * segment_duration)
} else {
segment_duration
};
playlist.push_str(&format!("#EXTINF:{seg_dur:.3},\n"));
playlist.push_str(&format!(
"/api/v1/media/{id}/stream/hls/{profile}/segment{i}.ts\n"
));
}
playlist.push_str("#EXT-X-ENDLIST\n");
Ok(axum::response::Response::builder()
.header("Content-Type", "application/vnd.apple.mpegurl")
.body(axum::body::Body::from(playlist))
.unwrap())
Ok(
axum::response::Response::builder()
.header("Content-Type", "application/vnd.apple.mpegurl")
.body(axum::body::Body::from(playlist))
.unwrap(),
)
}
pub async fn hls_segment(
State(state): State<AppState>,
Path((id, profile, segment)): Path<(Uuid, String, String)>,
State(state): State<AppState>,
Path((id, profile, segment)): Path<(Uuid, String, String)>,
) -> Result<axum::response::Response, ApiError> {
// Strict validation: reject path traversal, null bytes, leading dots
if segment.is_empty()
|| segment.starts_with('.')
|| segment.contains('\0')
|| segment.contains("..")
|| segment.contains('/')
|| segment.contains('\\')
{
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation("invalid segment name".into()),
));
// Strict validation: reject path traversal, null bytes, leading dots
if segment.is_empty()
|| segment.starts_with('.')
|| segment.contains('\0')
|| segment.contains("..")
|| segment.contains('/')
|| segment.contains('\\')
{
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"invalid segment name".into(),
),
));
}
let media_id = MediaId(id);
// Look for an active/completed transcode session
if let Some(transcode_service) = &state.transcode_service
&& let Some(session) =
transcode_service.find_session(media_id, &profile).await
{
let segment_path = session.cache_path.join(&segment);
if segment_path.exists() {
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to read segment: {}", e),
))
})?;
return Ok(
axum::response::Response::builder()
.header("Content-Type", "video/MP2T")
.body(axum::body::Body::from(data))
.unwrap(),
);
}
let media_id = MediaId(id);
// Session exists but segment not ready yet
return Ok(
axum::response::Response::builder()
.status(StatusCode::ACCEPTED)
.header("Retry-After", "2")
.body(axum::body::Body::from("segment not yet available"))
.unwrap(),
);
}
// Look for an active/completed transcode session
if let Some(transcode_service) = &state.transcode_service
&& let Some(session) = transcode_service.find_session(media_id, &profile).await
{
let segment_path = session.cache_path.join(&segment);
if segment_path.exists() {
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to read segment: {}", e),
))
})?;
return Ok(axum::response::Response::builder()
.header("Content-Type", "video/MP2T")
.body(axum::body::Body::from(data))
.unwrap());
}
// Session exists but segment not ready yet
return Ok(axum::response::Response::builder()
.status(StatusCode::ACCEPTED)
.header("Retry-After", "2")
.body(axum::body::Body::from("segment not yet available"))
.unwrap());
}
Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"no transcode session found; start a transcode first via POST /media/{id}/transcode"
.into(),
),
))
Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"no transcode session found; start a transcode first via POST \
/media/{id}/transcode"
.into(),
),
))
}
pub async fn dash_manifest(
State(state): State<AppState>,
Path(id): Path<Uuid>,
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<axum::response::Response, ApiError> {
let item = state.storage.get_media(MediaId(id)).await?;
let duration = item.duration_secs.unwrap_or(0.0);
if duration <= 0.0 {
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"cannot generate DASH manifest for media with unknown or zero duration".into(),
),
));
}
let hours = (duration / 3600.0) as u32;
let minutes = ((duration % 3600.0) / 60.0) as u32;
let seconds = duration % 60.0;
let item = state.storage.get_media(MediaId(id)).await?;
let duration = item.duration_secs.unwrap_or(0.0);
if duration <= 0.0 {
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"cannot generate DASH manifest for media with unknown or zero duration"
.into(),
),
));
}
let hours = (duration / 3600.0) as u32;
let minutes = ((duration % 3600.0) / 60.0) as u32;
let seconds = duration % 60.0;
let config = state.config.read().await;
let profiles = &config.transcoding.profiles;
let config = state.config.read().await;
let profiles = &config.transcoding.profiles;
let mut representations = String::new();
for profile in profiles {
let (w, h) = parse_resolution(&profile.max_resolution);
let bandwidth = estimate_bandwidth(profile);
let xml_name = escape_xml(&profile.name);
let url_name = utf8_percent_encode(&profile.name, NON_ALPHANUMERIC).to_string();
representations.push_str(&format!(
let mut representations = String::new();
for profile in profiles {
let (w, h) = parse_resolution(&profile.max_resolution);
let bandwidth = estimate_bandwidth(profile);
let xml_name = escape_xml(&profile.name);
let url_name =
utf8_percent_encode(&profile.name, NON_ALPHANUMERIC).to_string();
representations.push_str(&format!(
r#" <Representation id="{xml_name}" bandwidth="{bandwidth}" width="{w}" height="{h}">
<SegmentTemplate media="/api/v1/media/{id}/stream/dash/{url_name}/segment$Number$.m4s" initialization="/api/v1/media/{id}/stream/dash/{url_name}/init.mp4" duration="10000" timescale="1000" startNumber="0"/>
</Representation>
"#,
));
}
}
let mpd = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
let mpd = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="static" mediaPresentationDuration="PT{hours}H{minutes}M{seconds:.1}S" minBufferTime="PT1.5S">
<Period>
<AdaptationSet mimeType="video/mp4" segmentAlignment="true">
{representations} </AdaptationSet>
</Period>
</MPD>"#
);
);
Ok(axum::response::Response::builder()
.header("Content-Type", "application/dash+xml")
.body(axum::body::Body::from(mpd))
.unwrap())
Ok(
axum::response::Response::builder()
.header("Content-Type", "application/dash+xml")
.body(axum::body::Body::from(mpd))
.unwrap(),
)
}
pub async fn dash_segment(
State(state): State<AppState>,
Path((id, profile, segment)): Path<(Uuid, String, String)>,
State(state): State<AppState>,
Path((id, profile, segment)): Path<(Uuid, String, String)>,
) -> Result<axum::response::Response, ApiError> {
// Strict validation: reject path traversal, null bytes, leading dots
if segment.is_empty()
|| segment.starts_with('.')
|| segment.contains('\0')
|| segment.contains("..")
|| segment.contains('/')
|| segment.contains('\\')
{
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation("invalid segment name".into()),
));
// Strict validation: reject path traversal, null bytes, leading dots
if segment.is_empty()
|| segment.starts_with('.')
|| segment.contains('\0')
|| segment.contains("..")
|| segment.contains('/')
|| segment.contains('\\')
{
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"invalid segment name".into(),
),
));
}
let media_id = MediaId(id);
if let Some(transcode_service) = &state.transcode_service
&& let Some(session) =
transcode_service.find_session(media_id, &profile).await
{
let segment_path = session.cache_path.join(&segment);
if segment_path.exists() {
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to read segment: {}", e),
))
})?;
return Ok(
axum::response::Response::builder()
.header("Content-Type", "video/mp4")
.body(axum::body::Body::from(data))
.unwrap(),
);
}
let media_id = MediaId(id);
return Ok(
axum::response::Response::builder()
.status(StatusCode::ACCEPTED)
.header("Retry-After", "2")
.body(axum::body::Body::from("segment not yet available"))
.unwrap(),
);
}
if let Some(transcode_service) = &state.transcode_service
&& let Some(session) = transcode_service.find_session(media_id, &profile).await
{
let segment_path = session.cache_path.join(&segment);
if segment_path.exists() {
let data = tokio::fs::read(&segment_path).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to read segment: {}", e),
))
})?;
return Ok(axum::response::Response::builder()
.header("Content-Type", "video/mp4")
.body(axum::body::Body::from(data))
.unwrap());
}
return Ok(axum::response::Response::builder()
.status(StatusCode::ACCEPTED)
.header("Retry-After", "2")
.body(axum::body::Body::from("segment not yet available"))
.unwrap());
}
Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"no transcode session found; start a transcode first via POST /media/{id}/transcode"
.into(),
),
))
Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"no transcode session found; start a transcode first via POST \
/media/{id}/transcode"
.into(),
),
))
}