Skip to content
Open
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
85 changes: 79 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,77 @@ on:
push:
tags:
- 'v*'
branches:
- 'v*'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: moviebox-release-${{ github.ref_name }}
cancel-in-progress: true

jobs:
create-release:
name: Create GitHub release
if: startsWith(github.ref_name, 'v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Delete existing v0.1.24 release and tag for rebuild
if: github.ref_name == 'v0.1.24'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
gh release delete "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --yes --cleanup-tag=true || true

- name: Align release tag with branch commit
if: github.ref_type == 'branch'
shell: bash
env:
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -fa "${RELEASE_TAG}" "${RELEASE_SHA}" -m "MovieBox ${RELEASE_TAG}"
git push origin "refs/tags/${RELEASE_TAG}" --force

- name: Remove previous release assets
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json assets --jq '.assets[].name' |
while IFS= read -r asset_name; do
[ -z "${asset_name}" ] && continue
gh release delete-asset "${RELEASE_TAG}" "${asset_name}" --repo "${GITHUB_REPOSITORY}" --yes ||
echo "Asset ${asset_name} disappeared before cleanup; continuing."
done
fi

- name: Create release
uses: taiki-e/create-gh-release-action@v1
with:
changelog: CHANGELOG.md

ref: refs/tags/${{ github.ref_name }}
upload-assets:
name: Build ${{ matrix.name }}
if: github.ref_type == 'branch' || github.ref_type == 'tag'
needs: create-release
strategy:
fail-fast: false
Expand All @@ -47,11 +97,10 @@ jobs:
target: x86_64-pc-windows-msvc
os: windows-latest
archive: MovieBox_Windows_x64
- name: Android arm64
target: aarch64-linux-android
os: ubuntu-latest
archive: MovieBox_Android_arm64
build-tool: cross
- name: Windows arm64
target: aarch64-pc-windows-msvc
os: windows-latest
archive: MovieBox_Windows_arm64

runs-on: ${{ matrix.os }}
permissions:
Expand All @@ -69,6 +118,30 @@ jobs:
build-tool: ${{ matrix.build-tool || '' }}
locked: true
checksum: sha256
ref: refs/tags/${{ github.ref_name }}
include: LICENSE-MIT,LICENSE-APACHE,README.md
tar: unix
zip: windows
publish-checksums:
name: Publish SHA256SUMS
if: github.ref_type == 'branch' || github.ref_type == 'tag'
needs: upload-assets
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download per-asset checksums
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p checksums
gh release download "${GITHUB_REF_NAME}" --pattern "*.sha256" --dir checksums --repo "${GITHUB_REPOSITORY}"
- name: Build aggregate checksum file
run: |
set -euo pipefail
cat checksums/*.sha256 | sort > SHA256SUMS
- name: Upload aggregate checksum file
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${GITHUB_REF_NAME}" SHA256SUMS --clobber --repo "${GITHUB_REPOSITORY}"
2 changes: 2 additions & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ MovieBox is created and maintained by **Mrminecope**.

Project repository: https://github.com/Mrminecope/MovieBox
Project website: https://mrminecope.github.io/MovieBox/

Release 0.1.24 packaging and publication: **Mrminecope**.
12 changes: 7 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Changelog

## [0.1.24] - 2026-09-23
## [0.1.24] - 2026-09-24

### Fixed
- **VLC Seek / Buffer Recovery**:
- Increased VLC network caching to 5 seconds for remote MovieBox streams so seeking has enough buffer headroom instead of appearing frozen.
- Enabled VLC HTTP reconnect so dropped HTTP connections during seeks can recover automatically.
- Preserved `If-Range`, `ETag`, and `Last-Modified` metadata through the MovieBox loopback proxy and advertised byte-range support for `206 Partial Content` responses, improving reliable HTTP seeking for proxied DASH/media streams.
- **Playback compatibility rebuild**:
- Rebuilt v0.1.24 from the working v0.1.23 playback baseline after the first 0.1.24 build could fail to launch media on some player/client combinations.
- Restored the proven VLC and MovieBox proxy playback paths from v0.1.23.
- Removed the experimental VLC seek/reconnect flags and proxy validator changes from the previous 0.1.24 build.
- **Release identity**:
- Package, binary, repository, and updater identity remain `moviebox`, `moviebox.exe`, and `Mrminecope/MovieBox`.

## [0.1.23] - 2026-09-21

Expand Down
26 changes: 10 additions & 16 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,10 +630,6 @@ fn vlc_command(
}
command.arg("--play-and-exit");
command.arg("--adaptive-logic=highest");
// Give VLC enough network headroom for remote DASH seeks and reconnect
// automatically if a seek causes the current HTTP connection to drop.
command.arg("--network-caching=5000");
command.arg("--http-reconnect");
if let Some(start) = resume_seconds {
if start > 0 {
command.arg(format!("--start-time={start}"));
Expand Down Expand Up @@ -1452,21 +1448,20 @@ mod tests {

#[test]
fn test_format_mpv_script_opts_windows_paths() {
let win_path =
PathBuf::from(r"C:\Users\User\AppData\Local\MovieBox\playback\moviebox_123_1_1.json");
let win_path = PathBuf::from(
r"C:\Users\User\AppData\Local\MovieBox-Tui\playback\moviebox_123_1_1.json",
);
let opts = format_mpv_script_opts("moviebox", "123", 1, 1, &win_path);
assert!(!opts.contains(r"\"));
assert!(opts.contains("moviebox-state_file=C:/Users/User/AppData/Local/MovieBox/playback/moviebox_123_1_1.json"));
assert!(opts.contains("moviebox-state_file=C:/Users/User/AppData/Local/MovieBox-Tui/playback/moviebox_123_1_1.json"));
}

#[test]
fn test_format_mpv_script_opts_unix_paths() {
let unix_path =
PathBuf::from("/home/user/.local/share/moviebox/playback/moviebox_123_1_1.json");
PathBuf::from("/home/user/.local/share/moviebox-tui/playback/moviebox_123_1_1.json");
let opts = format_mpv_script_opts("moviebox", "123", 1, 1, &unix_path);
assert!(opts.contains(
"moviebox-state_file=/home/user/.local/share/moviebox/playback/moviebox_123_1_1.json"
));
assert!(opts.contains("moviebox-state_file=/home/user/.local/share/moviebox-tui/playback/moviebox_123_1_1.json"));
}

#[test]
Expand All @@ -1490,9 +1485,6 @@ mod tests {
assert!(args.contains(&"--width=1280".into()));
assert!(args.contains(&"--height=720".into()));
assert!(args.contains(&"--play-and-exit".into()));
assert!(args.contains(&"--adaptive-logic=highest".into()));
assert!(args.contains(&"--network-caching=5000".into()));
assert!(args.contains(&"--http-reconnect".into()));
assert!(args.contains(&"--start-time=42".into()));
assert!(args.contains(&"--http-referrer=https://example.test/".into()));
assert!(args.contains(&"--http-user-agent=MovieBox-Test".into()));
Expand All @@ -1508,7 +1500,7 @@ mod tests {
fn vlc_command_normalizes_windows_subtitle_paths() {
let command = vlc_command(
"https://example.test/video.mp4",
Some(r"C:\Users\User\AppData\Local\MovieBox\subs\sub.srt"),
Some(r"C:\Users\User\AppData\Local\MovieBox-Tui\subs\sub.srt"),
&[],
None,
None,
Expand All @@ -1518,7 +1510,9 @@ mod tests {
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert!(
args.contains(&"--sub-file=C:/Users/User/AppData/Local/MovieBox/subs/sub.srt".into())
args.contains(
&"--sub-file=C:/Users/User/AppData/Local/MovieBox-Tui/subs/sub.srt".into()
)
);
}
#[test]
Expand Down
47 changes: 4 additions & 43 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ async fn handle_connection(
let path_and_query = parts.next().unwrap_or("/");

let mut range_header = None;
let mut if_range_header = None;
let mut header_count = 0usize;
loop {
let mut header_line = String::new();
Expand All @@ -252,8 +251,6 @@ async fn handle_connection(
if let Some((name, val)) = trimmed.split_once(':') {
if name.trim().eq_ignore_ascii_case("range") {
range_header = Some(val.trim().to_string());
} else if name.trim().eq_ignore_ascii_case("if-range") {
if_range_header = Some(val.trim().to_string());
}
}
}
Expand Down Expand Up @@ -299,9 +296,6 @@ async fn handle_connection(
if let Some(range) = range_header {
req = req.header("Range", range);
}
if let Some(if_range) = if_range_header {
req = req.header("If-Range", if_range);
}

let upstream_res = match req.send().await {
Ok(res) => res,
Expand Down Expand Up @@ -369,8 +363,7 @@ async fn handle_connection(
return Ok(());
}

let headers_bytes =
format_proxy_response_headers(upstream_res.headers(), &target_url, status == reqwest::StatusCode::PARTIAL_CONTENT);
let headers_bytes = format_proxy_response_headers(upstream_res.headers(), &target_url);
writer.write_all(&headers_bytes).await?;

let mut stream = upstream_res.bytes_stream();
Expand All @@ -391,7 +384,6 @@ async fn handle_connection(
fn format_proxy_response_headers(
headers: &reqwest::header::HeaderMap,
target_url: &str,
is_partial_content: bool,
) -> Vec<u8> {
let mut out = Vec::new();
let clean_path = target_url
Expand All @@ -414,8 +406,6 @@ fn format_proxy_response_headers(
|| name_str.eq_ignore_ascii_case("content-length")
|| name_str.eq_ignore_ascii_case("content-range")
|| name_str.eq_ignore_ascii_case("accept-ranges")
|| name_str.eq_ignore_ascii_case("etag")
|| name_str.eq_ignore_ascii_case("last-modified")
{
if let Ok(val_str) = header_val.to_str() {
out.extend_from_slice(format!("{name_str}: {val_str}\r\n").as_bytes());
Expand All @@ -429,13 +419,6 @@ fn format_proxy_response_headers(
} else if is_vtt {
out.extend_from_slice(b"Content-Type: text/vtt\r\n");
}
if is_partial_content
&& !headers
.keys()
.any(|name| name.eq_ignore_ascii_case("accept-ranges"))
{
out.extend_from_slice(b"Accept-Ranges: bytes\r\n");
}
out.extend_from_slice(b"Connection: close\r\n\r\n");
out
}
Expand Down Expand Up @@ -638,8 +621,7 @@ mod tests {
headers.insert("server", "cloudflare".parse().unwrap());

let url = "https://cdn.example.com/subs.SRT?token=abc123&expires=999#top";
let out =
String::from_utf8(format_proxy_response_headers(&headers, url, false)).unwrap();
let out = String::from_utf8(format_proxy_response_headers(&headers, url)).unwrap();

assert_eq!(out.matches("Access-Control-Allow-Origin: *").count(), 1);
assert_eq!(out.matches("Content-Type: application/x-subrip").count(), 1);
Expand All @@ -656,8 +638,7 @@ mod tests {
headers.insert("content-length", "500".parse().unwrap());

let url = "https://cdn.example.com/subs.vtt";
let out =
String::from_utf8(format_proxy_response_headers(&headers, url, false)).unwrap();
let out = String::from_utf8(format_proxy_response_headers(&headers, url)).unwrap();

assert_eq!(out.matches("Access-Control-Allow-Origin: *").count(), 1);
assert_eq!(out.matches("Content-Type: text/vtt").count(), 1);
Expand All @@ -672,31 +653,11 @@ mod tests {
headers.insert("content-length", "10000000".parse().unwrap());

let url = "https://cdn.example.com/video.mp4";
let out =
String::from_utf8(format_proxy_response_headers(&headers, url, false)).unwrap();
let out = String::from_utf8(format_proxy_response_headers(&headers, url)).unwrap();

assert_eq!(out.matches("Access-Control-Allow-Origin: *").count(), 1);
assert!(out.contains("content-type: video/mp4"));
assert!(!out.contains("application/x-subrip"));
assert!(!out.contains("text/vtt"));
}

#[test]
fn test_format_proxy_response_headers_for_partial_content() {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("content-type", "video/mp4".parse().unwrap());
headers.insert("content-length", "1000".parse().unwrap());
headers.insert("content-range", "bytes 1000-1999/10000".parse().unwrap());
headers.insert("etag", ""movie-v1"".parse().unwrap());
headers.insert("last-modified", "Wed, 23 Sep 2026 08:00:00 GMT".parse().unwrap());

let url = "https://cdn.example.com/video.mp4";
let out =
String::from_utf8(format_proxy_response_headers(&headers, url, true)).unwrap();

assert!(out.contains("content-range: bytes 1000-1999/10000"));
assert!(out.contains("etag: "movie-v1""));
assert!(out.contains("last-modified: Wed, 23 Sep 2026 08:00:00 GMT"));
assert!(out.contains("Accept-Ranges: bytes"));
}
}
Loading