diff --git a/.github/actions/libmpv-windows/action.yml b/.github/actions/libmpv-windows/action.yml new file mode 100644 index 0000000..c8d21c1 --- /dev/null +++ b/.github/actions/libmpv-windows/action.yml @@ -0,0 +1,51 @@ +name: libmpv (Windows) +description: > + Provide libmpv's Windows development files. `libmpv2-sys` emits + `cargo:rustc-link-lib=mpv`, and mpv's Windows *player* build ships neither + mpv.lib nor libmpv-2.dll — they come from the separate mpv-dev archive. Sets + LIB for the linker, PATH so binaries find the DLL at run time, and + MPV_DEV_DIR for `scripts/build.ps1`. + +outputs: + dir: + description: Directory holding libmpv-2.dll and mpv.lib. + value: ${{ steps.mpv.outputs.dir }} + +runs: + using: composite + steps: + # lib.exe, for turning mpv.def into an import library. + - uses: ilammy/msvc-dev-cmd@v1 + + - id: mpv + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $feed = [xml](Invoke-WebRequest -UseBasicParsing ` + 'https://sourceforge.net/projects/mpv-player-windows/rss?path=/libmpv').Content + # The "v3" variants are x86-64-v3 builds; take the baseline one. + $item = $feed.rss.channel.item | + Where-Object { $_.link -match 'mpv-dev-x86_64-2[^/]*\.7z/download$' } | + Select-Object -First 1 + if (-not $item) { throw 'no mpv-dev build in the SourceForge feed' } + + $archive = Join-Path $env:RUNNER_TEMP 'mpv-dev.7z' + $dir = Join-Path $env:RUNNER_TEMP 'mpv-dev' + Invoke-WebRequest -UseBasicParsing $item.link -OutFile $archive + 7z x $archive "-o$dir" -y | Out-Null + + $dll = Get-ChildItem -Path $dir -Filter 'libmpv-2.dll' -Recurse | + Select-Object -First 1 + if (-not $dll) { throw "libmpv-2.dll not found under $dir" } + $libDir = $dll.Directory.FullName + + # /NAME matters: without it the import library references mpv.dll, and + # the file that actually ships is libmpv-2.dll. + lib.exe /def:"$libDir\mpv.def" /name:libmpv-2.dll ` + /out:"$libDir\mpv.lib" /machine:x64 | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'lib.exe failed to build mpv.lib' } + + "dir=$libDir" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + "MPV_DEV_DIR=$libDir" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + "LIB=$libDir;$env:LIB" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + $libDir | Out-File $env:GITHUB_PATH -Append -Encoding utf8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a591599..e0e9b1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,12 @@ jobs: sudo apt-get update sudo apt-get install -y libmpv-dev + # Without this the test binaries do not link: there is no mpv.lib on the + # runner. See .github/actions/libmpv-windows. + - name: Install libmpv (Windows) + if: runner.os == 'Windows' + uses: ./.github/actions/libmpv-windows + - run: cargo fmt --all -- --check - run: cargo clippy --workspace --all-targets -- -D warnings diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3cf1fe1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,153 @@ +name: Release + +# A release is a `v` tag on main. The tag is the trigger and the +# version of record: nothing here reads the branch, so the first job checks that +# the commit really is on main and that the tag, `Cargo.toml` and the PKGBUILD +# all say the same version — a tag that disagrees would otherwise publish +# artifacts named after the manifest, not after the tag. +# +# `workflow_dispatch` builds everything and publishes nothing, which is how to +# rehearse a release without burning a tag. + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + verify: + name: Tag and versions + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: The tag is on main + if: startsWith(github.ref, 'refs/tags/') + run: | + git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main + git merge-base --is-ancestor "$GITHUB_SHA" origin/main || + { echo "::error::$GITHUB_REF_NAME points at a commit that is not on main"; exit 1; } + + - name: The tag, Cargo.toml and the PKGBUILD agree + if: startsWith(github.ref, 'refs/tags/') + run: | + tag="${GITHUB_REF_NAME#v}" + manifest="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)" + pkgbuild="$(sed -n 's/^pkgver=//p' packaging/PKGBUILD | head -n1)" + echo "tag=$tag manifest=$manifest pkgbuild=$pkgbuild" + [ "$tag" = "$manifest" ] || + { echo "::error::tag $tag != Cargo.toml $manifest"; exit 1; } + [ "$tag" = "$pkgbuild" ] || + { echo "::error::tag $tag != PKGBUILD $pkgbuild"; exit 1; } + + # A release must resolve the SDK from crates.io. The build scripts refuse + # to run with a patch pointing at a missing sibling checkout, but here the + # patch is wrong even when the path exists. + - name: No path patch on the SDK + run: | + ! grep -q '^\[patch\.crates-io\]' Cargo.toml || + { echo "::error::Cargo.toml carries [patch.crates-io]; releases build against the registry"; exit 1; } + + linux: + name: Linux artifacts + needs: verify + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + # Pinned: the MSRV the workspace declares. + toolchain: "1.96" + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + # rpm for rpmbuild; the .deb goes through dpkg-deb, which is preinstalled. + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libmpv-dev rpm + + - name: Gate + run: | + cargo fmt --all -- --check + cargo clippy --workspace --all-targets -- -D warnings + cargo test --workspace --locked + + - name: Build tarball, .deb and .rpm + run: scripts/build.sh tarball deb rpm + + - uses: actions/upload-artifact@v4 + with: + name: linux + path: | + dist/*.tar.gz + dist/*.deb + dist/*.rpm + if-no-files-found: error + + windows: + name: Windows artifacts + needs: verify + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: "1.96" + + - uses: Swatinem/rust-cache@v2 + + # Sets MPV_DEV_DIR, which is what build.ps1 reads when -MpvDev is absent. + - name: Install libmpv + uses: ./.github/actions/libmpv-windows + + # build.ps1 installs the WiX toolset itself if `wix` is missing, and the + # runner has the .NET SDK it needs for that. + - name: Build the portable .zip and the .msi + shell: pwsh + run: scripts\build.ps1 -Artifacts zip,msi + + - uses: actions/upload-artifact@v4 + with: + name: windows + path: | + dist/*.zip + dist/*.msi + if-no-files-found: error + + publish: + name: Publish + if: startsWith(github.ref, 'refs/tags/') + needs: [linux, windows] + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Checksums + run: | + cd artifacts + sha256sum * > SHA256SUMS + cat SHA256SUMS + + - uses: softprops/action-gh-release@v2 + with: + files: artifacts/* + generate_release_notes: true + fail_on_unmatched_files: true diff --git a/Cargo.lock b/Cargo.lock index 62a6ed5..c1b8a29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4037,6 +4037,8 @@ checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" [[package]] name = "proton-drive-rs" version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b162681255e296b48ef7261cad899ffb384756e866a1c6d7b7c791cbbc8af3ac" dependencies = [ "async-trait", "base64 0.22.1", @@ -4062,6 +4064,8 @@ dependencies = [ [[package]] name = "proton-sdk" version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d9b94ec901edb0e849238ea833121f2fb173308b4927b8aa6b157643fa0a73" dependencies = [ "aes-gcm 0.11.0", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index b446db4..796c49c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,9 +91,11 @@ lru = "0.12" # be tested against a fake without a network or an account. async-trait = "0.1" -# During development the SDK work for this app lives in the sibling repo and is -# not published yet. Remove once 0.3.3 is on crates.io — releases must build -# against the registry. -[patch.crates-io] -proton-sdk = { path = "../proton-sdk-rs/crates/proton-sdk" } -proton-drive-rs = { path = "../proton-sdk-rs/crates/proton-drive-rs" } +# The SDK builds against the registry — 0.4.0 is published. To test an +# unpublished `../proton-sdk-rs` change against this app, add locally and do +# **not** commit it (CI has no sibling checkout, and the build scripts refuse to +# run with a patch pointing at a missing path): +# +# [patch.crates-io] +# proton-sdk = { path = "../proton-sdk-rs/crates/proton-sdk" } +# proton-drive-rs = { path = "../proton-sdk-rs/crates/proton-drive-rs" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e51bd8d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Nils Pukropp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f5909ef..6728ea9 100644 --- a/README.md +++ b/README.md @@ -18,23 +18,28 @@ is playable without downloading 2 GB of it. ## Status -Early, but it plays, and there is a window now. The SDK streaming surface, the -catalog, the streaming layer and the player are done and verified against a real -share; the app has a library wall, a page per title with seasons and episodes, -resume-where-you-left-off, a transport bar and share management. The picture -still lands in mpv's own window — embedding it in the app's surface is next. See -`docs/DEVELOPMENT.md`. +Early, but it is an app now: the picture lands **inside the window**, not in an +mpv window of its own. The SDK streaming surface, the catalog, the streaming +layer and the player are done and verified against a real share, and the app has +a library wall, a page per title with seasons and episodes, +resume-where-you-left-off, an overlay with a chapter-marked seek bar, a skip +button for openings and endings, next/previous episode with an "up next" +countdown, optional metadata, themes and share management. What is left is mostly sidecar subtitle files +(`.srt`/`.ass` beside the video are invisible to the crawl today) and running the +macOS packaging on a real Mac. See `docs/DEVELOPMENT.md`. ```bash cargo run --release -p pstr-app # the app ``` -Working today, via the `pstr` CLI: +The `pstr` CLI does the same work headlessly, and is what tells you which layer +is broken: ```bash cargo run -p pstr-cli -- add --name anime 'https://drive.proton.me/urls/TOKEN#fragment' cargo run -p pstr-cli -- crawl cargo run -p pstr-cli -- list +cargo run -p pstr-cli -- metadata # opt-in; off unless configured cargo run --release -p pstr-cli -- play --file Frieren --disk-cache cargo run --release -p pstr-cli -- bench --file Frieren --verify ``` @@ -54,12 +59,12 @@ against the SDK's own range download. |---|---| | Topology | Fat client. No server; each viewer's app talks to Proton directly. | | Auth | Public links only — but several, merged into one library. | -| Player | Embedded libmpv: HEVC 10-bit, HDR and ASS subtitles with hardware decode. Volume, audio-track, subtitle and chapter pickers; skip-the-opening; next/previous episode and autoplay. The language picked is preferred for the next file too. | +| Player | Embedded libmpv, drawing into the app's own GL surface: HEVC 10-bit, HDR and ASS subtitles with hardware decode. Volume, audio-track, subtitle and chapter pickers; skip-the-opening; next/previous episode and autoplay. The language picked is preferred for the next file too. | | Transcoding | None. mpv demuxes and decodes natively. | | UI | egui/eframe on glow; mpv renders into the same GL context. | | Themes | The shipped near-black palette plus Catppuccin Latte, Frappé, Macchiato and Mocha, each with a choice of eight accents — pink and sky among them — drawn as a gradient unless you turn that off. Picked on the Shares page, applied without a restart. | -| Metadata | Filename parsing, with optional AniList/TMDB enrichment — **off by default**, because enabling it sends your library's titles to a third party. | -| Platforms | Linux and Windows. | +| Metadata | Filename parsing, with optional AniList/TMDB enrichment — **off by default**, because enabling it sends your library's titles to a third party. It brings posters, synopses and per-episode titles; a title the scorer refuses to guess about can be pinned by hand, and a pinned match is never overwritten. | +| Platforms | Linux and Windows. The macOS packaging is written but has not been run on a Mac. | ## Building @@ -69,8 +74,15 @@ Rust 2024, MSRV 1.96. cargo build --release ``` -`pstr-player` needs libmpv development headers (`mpv` ≥ 2.0). On Windows, -`libmpv-2.dll` is bundled next to the executable. +`pstr-player` needs libmpv development headers (`mpv` ≥ 2.0). On Windows those +come from the separate **mpv-dev** archive rather than mpv's player build, and +`libmpv-2.dll` ships next to the executable. The Proton SDK crates come from +crates.io; nothing here builds against a sibling checkout. + +Installable artifacts — tarball, `.deb`, `.rpm`, Arch package, `.app`/`.dmg`, +and a portable `.zip` plus an `.msi` on Windows — are produced by +`scripts/build.sh` and `scripts\build.ps1`; `packaging/README.md` has the +per-platform detail. ## Privacy diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 92858d5..2640af0 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4,34 +4,39 @@ | Layer | State | |---|---| -| SDK streaming surface (`../proton-sdk-rs` 0.3.3) | **done, live-verified** — `open_revision` / `download_range` / thumbnails / `refresh_session` on the visitor path; node-key cache; chunked `enumerate_nodes`; paged `download_file_to` | -| `pstr-core` — shares, config, catalog, naming, library grouping | **done** — 67 offline tests | -| `pstr-cli` — add / crawl / list | **done** — verified against a real 354-node share: 310 playable files, 22 titles, correct seasons and episodes | +| SDK streaming surface (`../proton-sdk-rs` 0.4.0, from crates.io) | **done, live-verified** — `open_revision` / `download_range` / thumbnails / `refresh_session` on the visitor path; node-key cache; chunked `enumerate_nodes`; paged `download_file_to` | +| `pstr-core` — shares, config, catalog, naming, library grouping | **done** — 106 offline tests | +| `pstr-cli` — add / shares / remove / crawl / list / metadata / play / bench | **done** — verified against a real 354-node share: 310 playable files, 22 titles, correct seasons and episodes | | `pstr-stream` — block cache, read-ahead | **done, live-verified** — 68 offline tests; benchmarked against a real 761 MiB episode (see `docs/TESTING.md`) | -| `pstr-player` — libmpv `stream_cb` | **done, live-verified** — 16 offline tests; plays a real 1080p HEVC episode over a public link, seeks, resumes from the disk cache. The render API (embedding) is not started | -| `pstr-app` — egui UI, poster grid | **done, offline-tested** — library wall, title pages, transport, share management; mpv still opens its own window | -| Metadata providers (AniList / TMDB) | not started — and this is what the wall is waiting on: see the note on thumbnails below | -| Packaging | **done, locally verified** — `scripts/build.sh` (tarball, `.deb`, `.rpm`, `.app`, `.dmg`) and `scripts/build.ps1` (portable `.zip`, WiX `.msi`), plus `packaging/PKGBUILD`. See `packaging/README.md`. The Windows and macOS halves are written but only the Linux ones have been run on this machine | +| `pstr-player` — libmpv `stream_cb` + render API | **done, live-verified** — 33 offline tests; plays a real 1080p HEVC episode over a public link, seeks, resumes from the disk cache, and renders into the app's GL context | +| `pstr-app` — egui UI, poster grid, embedded video | **done, offline-tested** — 15 offline tests; library wall, title pages, player page with overlay controls, themes, share management | +| Metadata providers (AniList / TMDB) | **done** — 32 offline tests; posters, synopses, per-episode titles, hand-pinned matches. Opt-in, off by default | +| Packaging | **done, locally verified** — `scripts/build.sh` (tarball, `.deb`, `.rpm`, `.app`, `.dmg`) and `scripts/build.ps1` (portable `.zip`, WiX `.msi`), plus `packaging/PKGBUILD`. See `packaging/README.md`. The macOS half is written but has not been run on a Mac | ## Build order from here -1. **`pstr-player`, step two.** `mpv_render_context_create` with - `MPV_RENDER_API_TYPE_OPENGL` against eframe's glow context, rendered inside an - `egui::PaintCallback`; overlay controls. If this proves troublesome, the - standalone-window mode already built remains shippable. Nothing in - `pstr-player` except `PlayerConfig`'s window options is about where pixels - land, so this does not reach the streaming path. -2. Metadata providers, docs. Packaging is in place (`packaging/README.md`); what - is left there is running `scripts/build.ps1` and the macOS half on real - hosts, and a release workflow that drives both. +1. **Sidecar subtitle files** — see the "not done" note at the end of this + section. The work is in `pstr-core`: crawl them, and pair them to a video by + name. +2. Packaging is in place (`packaging/README.md`); what is left there is running + the macOS half on a real host, and a release workflow that drives every + platform. + +Done: **`pstr-player` step two** — `mpv_render_context_create` with +`MPV_RENDER_API_TYPE_OPENGL` against eframe's glow context +(`pstr-player/src/render.rs`), painted from a framebuffer this app owns +(`pstr-app/src/video.rs`) so the picture can sit in a rectangle rather than +filling the window. `VideoOutput::Embedded` is what the app asks for; the +standalone-window mode the CLI uses is unchanged, and nothing about it reaches +the streaming path. Done: `pstr-player` step one — `mpv_stream_cb_add_ro`, mpv in its own window. `pstr play --file ` plays a catalogued episode over a public link. Done: `pstr-app` step one — `cargo run -p pstr-app` opens a window with the library wall, a title page per series, a transport bar and share management. It -plays through the same `Player` the CLI uses, so mpv still owns its own window; -the app's transport drives it from a command channel. Two pieces of that are +plays through the same `Player` the CLI uses — at that point still in mpv's own +window; the app's transport drives it from a command channel. Two pieces of that are worth knowing before touching it: - **Drawing never mutates.** Pages are handed `&`-state and push `Action`s onto diff --git a/packaging/README.md b/packaging/README.md index cd811c7..40482bf 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -128,12 +128,29 @@ The `.icns` is rasterized from `io.narl.proton-stream.svg` when `rsvg-convert` o The Debian `control` file has no template — `scripts/build.sh` writes it, because almost every field in it is computed. -## Two things to know before releasing +## Releasing -- **The `[patch.crates-io]` block in `Cargo.toml` is development only.** It points - at `../proton-sdk-rs`, so both build scripts refuse to start when it is present - and that checkout is missing. A release must build against the published - crates — drop the block once the pinned proton-sdk is on crates.io. -- **There is no `LICENSE` file in the repository root** even though `Cargo.toml` - declares MIT. Every packager warns and ships without the license text; the MSI - falls back to `windows/license.rtf`. Adding the file fixes all of them at once. +A release is a `v` tag on `main`; `.github/workflows/release.yml` does +the rest. It runs the CI gate and `scripts/build.sh tarball deb rpm` on Linux, +`scripts\build.ps1 -Artifacts zip,msi` on Windows, and publishes both sets plus +a `SHA256SUMS` to a GitHub release. So the steps are: + +```bash +# 1. bump Cargo.toml's workspace version *and* packaging/PKGBUILD's pkgver +# 2. commit on main, then +git tag -a v0.1.0 -m 'proton-stream 0.1.0' +git push origin main --follow-tags +``` + +The workflow refuses to publish a tag whose commit is not an ancestor of `main`, +or whose version disagrees with `Cargo.toml` or the PKGBUILD — the artifacts are +named from the manifest, so a drifted tag would ship a file called something +other than the tag. It also refuses a `Cargo.toml` carrying `[patch.crates-io]`: +a release builds against the published SDK crates, never a sibling checkout. + +`workflow_dispatch` runs everything except the publish step, which is how to +rehearse without burning a tag. + +Not covered by the workflow: the Arch package (`makepkg` compiles in place, so +it wants an Arch host) and the macOS `.app`/`.dmg` (no runner for it here yet). +Both are still `scripts/build.sh arch` and `scripts/build.sh dmg` by hand.