Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ body:
attributes:
label: haki-dl Version
description: Which version are you using?
placeholder: "0.2.0"
placeholder: "0.3.0"
validations:
required: true

Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
# 0.3.0 (Jun 11, 2026)

- Added the optional `rpc` feature with a haki-native JSON-RPC server over HTTP POST and WebSocket, including `RpcServer`, `RpcServerBuilder`, `RpcSessionManager`, token/basic/TLS configuration, CORS control, and request-size limits.
- Added RPC download/session methods for adding downloads, queue-aware pause/resume/removal, status/list queries, option changes, queued URI replacement, global stats/options, shutdown, method discovery, notification discovery, and `system.multicall`.
- Added structured RPC notifications for download start, pause, stop, completion, error, and live progress events with gid-based status fields.
- Added queue and concurrency controls so RPC callers can enqueue many downloads while limiting active non-blocking async work.
- Added CLI RPC server mode and matching options for listen address/port, auth, TLS, request size, CORS, queue mode, and max concurrent downloads while keeping the public CLI parse result semver-compatible.
- Updated the public examples to use auto-cleaned temporary workspaces with `tempfile::TempDir`.

# 0.2.0 (Jun 10, 2026)

- Added CLI missing-input handling, including the required-argument message and full help output when no input is provided.
- Reworked CLI help rendering, value placeholders, aligned descriptions, wrapped long descriptions, and expanded `--morehelp` text for mux, import, selection, and range options.
- Preserved forced ANSI console behavior for redirected output so manual and comparison console runs can still validate colored output when explicitly requested.
- Preserved forced ANSI console behavior for redirected output so redirected console runs can still validate colored output when explicitly requested.
- Fixed ffmpeg concat-demuxer merge planning to use absolute, normalized input paths for portable generated concat lists.
- Enabled the release workflow semver check as a non-blocking pre-1.0 gate and made release binary/release jobs wait for it.

Expand Down
19 changes: 17 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "haki-dl"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
rust-version = "1.88"
authors = ["bakgio"]
Expand All @@ -20,13 +20,28 @@ rustdoc-args = ["--cfg", "docsrs"]
default = ["cli", "mp4forge"]
cli = ["dep:crossterm"]
mp4forge = ["dep:mp4forge"]
rpc = [
"dep:axum",
"dep:hyper",
"dep:hyper-util",
"dep:serde",
"dep:serde_json",
"dep:tokio-rustls",
"dep:tower-service",
]
serde = ["dep:serde"]

[dependencies]
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio", "ws"], optional = true }
hyper = { version = "1.10.1", default-features = false, features = ["http1", "server"], optional = true }
hyper-util = { version = "0.1.20", default-features = false, features = ["tokio"], optional = true }
tokio-rustls = { version = "0.26.4", optional = true }
tower-service = { version = "0.3.3", optional = true }
roxmltree = "0.21.1"
reqwest = { version = "0.13.4", default-features = false, features = ["rustls", "http2", "gzip", "deflate", "brotli", "zstd", "socks"] }
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "fs", "io-util", "process", "sync", "time"] }
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "fs", "io-util", "net", "process", "sync", "time"] }
regex = "1"
encoding_rs = "0.8"
flate2 = "1"
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@

```toml
[dependencies]
haki-dl = "0.2.0"
haki-dl = "0.3.0"
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros"] }

# With optional features:
# haki-dl = { version = "0.2.0", features = ["mp4forge"] }
# haki-dl = { version = "0.2.0", features = ["serde"] }
# haki-dl = { version = "0.3.0", features = ["mp4forge"] }
# haki-dl = { version = "0.3.0", features = ["rpc"] }
# haki-dl = { version = "0.3.0", features = ["serde"] }
# Minimal builds can opt out of defaults:
# haki-dl = { version = "0.2.0", default-features = false }
# haki-dl = { version = "0.3.0", default-features = false }
```

Install the CLI from crates.io:
Expand All @@ -60,6 +61,7 @@ API consumers need a Tokio runtime. The CLI creates its own runtime, but library

- `cli`: retained as the default CLI-capable package feature. The `haki-dl` binary is built from `src/main.rs` and is not hidden behind a `required-features` gate.
- `mp4forge`: enables the published `mp4forge` crate backend for default in-process MP4 decryption and explicit MP4-family mux requests. Muxing is not selected by default.
- `rpc`: enables the optional haki-native JSON-RPC server over HTTP POST and WebSocket, including queue-aware download control, status queries, and structured progress notifications.
- `serde`: derives `Serialize` and `Deserialize` for reusable public metadata/report types where exposed.

HLS, DASH, MSS, live, decrypt, capture, and license are not Cargo feature flags. Core protocol/decrypt/live behavior stays available in the library until there is a real, tested need for coarse minimal-build gates. ffmpeg is validated at session startup for compatibility runtime behavior. mkvmerge and external decrypt tools are runtime process tools selected through options such as `mkvmerge_binary_path`, `decryption_engine`, `decryption_binary_path`, and `mux_after_done`. They are intentionally not Cargo feature flags.
Expand Down
36 changes: 19 additions & 17 deletions examples/api_recipes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,39 @@ use haki_dl::{
DownloadRequest, MuxAfterDoneOptions, MuxFormat, MuxerKind, ProgressEvent, StreamSelector,
summarize_events,
};
use tempfile::TempDir;

#[tokio::main]
async fn main() -> haki_dl::Result<()> {
let workspace = prepare_workspace("haki_dl_api_recipes").await?;
let hls_input = write_hls_fixture(&workspace).await?;
let dash_input = write_dash_fixture(&workspace).await?;
let workspace = prepare_workspace("haki_dl_api_recipes")?;
let workspace_path = workspace.path();
let hls_input = write_hls_fixture(workspace_path).await?;
let dash_input = write_dash_fixture(workspace_path).await?;
let token = CancellationToken::new();
let mut recipes = vec![
("simple_hls", simple_hls_request(&hls_input, &workspace)),
("simple_dash", simple_dash_request(&dash_input, &workspace)),
("simple_hls", simple_hls_request(&hls_input, workspace_path)),
(
"simple_dash",
simple_dash_request(&dash_input, workspace_path),
),
(
"default_decrypt",
encrypted_request_with_default_decrypt(&dash_input, &workspace),
encrypted_request_with_default_decrypt(&dash_input, workspace_path),
),
(
"custom_headers_proxy",
custom_header_proxy_request(&hls_input, &workspace),
custom_header_proxy_request(&hls_input, workspace_path),
),
(
"cancellable_live",
cancellable_live_request(&hls_input, &workspace, token.clone()),
cancellable_live_request(&hls_input, workspace_path, token.clone()),
),
(
"explicit_mp4_mux",
explicit_mp4_mux_request(&dash_input, &workspace),
explicit_mp4_mux_request(&dash_input, workspace_path),
),
];
if let Some(request) = external_decrypt_request(&dash_input, &workspace) {
if let Some(request) = external_decrypt_request(&dash_input, workspace_path) {
recipes.push(("external_decrypt", request));
} else {
println!("external_decrypt skipped: ffmpeg was not found on PATH");
Expand All @@ -55,13 +60,10 @@ async fn main() -> haki_dl::Result<()> {
Ok(())
}

async fn prepare_workspace(name: &str) -> haki_dl::Result<PathBuf> {
let workspace = std::env::temp_dir().join(name);
if tokio::fs::metadata(&workspace).await.is_ok() {
tokio::fs::remove_dir_all(&workspace).await?;
}
tokio::fs::create_dir_all(&workspace).await?;
Ok(workspace)
fn prepare_workspace(name: &str) -> haki_dl::Result<TempDir> {
Ok(tempfile::Builder::new()
.prefix(&format!("{name}-"))
.tempdir()?)
}

async fn write_hls_fixture(workspace: &Path) -> haki_dl::Result<PathBuf> {
Expand Down
18 changes: 8 additions & 10 deletions examples/basic_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ use haki_dl::{
DownloadClient, DownloadOptions, DownloadRequest, ProgressCallback, ProgressEventCollector,
StreamSelector, summarize_events,
};
use tempfile::TempDir;

#[tokio::main]
async fn main() -> haki_dl::Result<()> {
let workspace = prepare_workspace("haki_dl_basic_api").await?;
let input = write_hls_fixture(&workspace).await?;
let workspace = prepare_workspace("haki_dl_basic_api")?;
let input = write_hls_fixture(workspace.path()).await?;
let callback_events = Arc::new(Mutex::new(ProgressEventCollector::new()));
let callback_sink = Arc::clone(&callback_events);

let request = DownloadRequest::new(input.to_string_lossy().into_owned())
.with_options(example_options(&workspace, "basic_api"))
.with_options(example_options(workspace.path(), "basic_api"))
.with_stream_selector(StreamSelector::Auto)
.with_progress_callback(ProgressCallback::new(move |event| {
if let Ok(mut collector) = callback_sink.lock() {
Expand All @@ -40,13 +41,10 @@ async fn main() -> haki_dl::Result<()> {
Ok(())
}

async fn prepare_workspace(name: &str) -> haki_dl::Result<PathBuf> {
let workspace = std::env::temp_dir().join(name);
if tokio::fs::metadata(&workspace).await.is_ok() {
tokio::fs::remove_dir_all(&workspace).await?;
}
tokio::fs::create_dir_all(&workspace).await?;
Ok(workspace)
fn prepare_workspace(name: &str) -> haki_dl::Result<TempDir> {
Ok(tempfile::Builder::new()
.prefix(&format!("{name}-"))
.tempdir()?)
}

async fn write_hls_fixture(workspace: &Path) -> haki_dl::Result<PathBuf> {
Expand Down
Loading
Loading