docs: user guide pages, published to the GitHub wiki
docs/ gains pages for people using the app rather than building it: getting started, installation, the user guide, account and sync, Android and iOS, the command line, privacy, an FAQ and troubleshooting. docs/README.md is the wiki's home page. scripts/publish-wiki.sh, ported from proton-drive-linux, checks every link in docs/ and README.md, rewrites links between pages to wiki page names and copies the images they show. The Wiki workflow runs the check on pull requests and publishes on every push to main. images/ holds library, title and episode screenshots from the desktop and Android apps, and the logo.
No files matched your search
@@ -0,0 +1,68 @@
|
||||
name: Wiki
|
||||
|
||||
# docs/ is the source of the GitHub wiki. Every change to the documentation
|
||||
# checks its links; a push to main publishes the result, so the wiki follows
|
||||
# main. Pull requests only check: nothing unmerged reaches the wiki. Run it from
|
||||
# the Actions tab to publish another ref, such as a release tag.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'images/**'
|
||||
- 'README.md'
|
||||
- 'scripts/publish-wiki.sh'
|
||||
- '.github/workflows/wiki.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'images/**'
|
||||
- 'README.md'
|
||||
- 'scripts/publish-wiki.sh'
|
||||
- '.github/workflows/wiki.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Tag or branch to publish'
|
||||
required: false
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check documentation links
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Check links
|
||||
run: scripts/publish-wiki.sh --check "$RUNNER_TEMP"
|
||||
|
||||
publish:
|
||||
name: Publish to the wiki
|
||||
needs: check
|
||||
# Pull requests never publish.
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
group: wiki-publish
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
WIKI_REMOTE: https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.wiki.git
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
scripts/publish-wiki.sh --push "$RUNNER_TEMP"
|
||||
@@ -0,0 +1,69 @@
|
||||
# Account and sync
|
||||
|
||||
Signing in to Proton is optional. Without it, the app plays public share links and keeps
|
||||
everything on one device. Signing in adds two things:
|
||||
|
||||
- **Folders of your own Drive** as shares, with no link to copy.
|
||||
- **Sync** of your place in every episode, your shares and your settings, across every device
|
||||
you sign in on.
|
||||
|
||||
## Signing in
|
||||
|
||||
The **Proton account** section is at the top of the **Shares** page.
|
||||
|
||||
1. Enter your Proton address and password, then **Sign in**.
|
||||
2. If your account has two-factor authentication, enter the code from your authenticator app.
|
||||
3. If your account has a separate mailbox password, enter it and choose **Unlock**.
|
||||
|
||||
Proton sometimes asks to confirm you are human. The app then opens a small window with Proton's
|
||||
own CAPTCHA page. Solve it, and sign-in continues.
|
||||
|
||||
The app keeps a session, not your password. The session goes into the system keyring. **Sign
|
||||
out** ends it on Proton's side and deletes it here.
|
||||
|
||||
Not supported yet:
|
||||
|
||||
- **Security keys** (FIDO2). An account whose only second factor is a security key cannot sign
|
||||
in. Add an authenticator app as a second method in your Proton settings.
|
||||
- **Email or SMS verification** instead of a CAPTCHA.
|
||||
|
||||
## Adding a folder of your Drive
|
||||
|
||||
1. Sign in.
|
||||
2. Under **Add from your Drive**, choose **Browse your Drive**.
|
||||
3. Open folders until you reach the one that holds your videos.
|
||||
4. Choose **Add this folder**.
|
||||
|
||||
The folder becomes a share like any link: it is crawled, shown in the library and streamed. It is
|
||||
read through your account, so there is no link or password to store. The same folder has the
|
||||
same identity on every device, so watch history follows it.
|
||||
|
||||
## Sync
|
||||
|
||||
When you are signed in, each device keeps a small file in your Drive. You find them under
|
||||
**Computers** on the Proton Drive website, in a device called `proton-stream`, in the folder
|
||||
`watch-history`.
|
||||
|
||||
**What syncs:**
|
||||
|
||||
- Where you stopped in each episode, and which episodes you watched
|
||||
- Your shares, including their links and passwords (end-to-end encrypted, like every file in
|
||||
your Drive)
|
||||
- Settings: playback, appearance and posters and descriptions, including a TMDB key
|
||||
- Matches you picked by hand, and your audio and subtitle choice for each show
|
||||
|
||||
**What stays on each device:** volume and mute, the streaming cache size, text size, Wi-Fi only,
|
||||
background audio, hardware decoding, window size, and the library's filter and sort order.
|
||||
|
||||
**When it syncs:** when the app starts, every 5 minutes while it is open, when you stop playing,
|
||||
and when it closes. **Sync now** runs it at once. Android also syncs in the background shortly
|
||||
after you leave the app.
|
||||
|
||||
**How conflicts are settled:** each device writes only its own file, and reads everyone else's.
|
||||
For each episode and each setting, the newest change wins. A device that signs in for the first
|
||||
time takes the others' state and does not overwrite it with its own defaults.
|
||||
|
||||
A device that has not synced for 90 days has its file moved to the Drive trash.
|
||||
|
||||
The status line under your address says how the last sync went, for example *Synced 3 min ago ·
|
||||
2 positions from other devices*. To stop syncing, sign out.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Command-line reference
|
||||
|
||||
The desktop packages install two programs:
|
||||
|
||||
- `proton-stream`, the app
|
||||
- `pstr`, a command-line tool that does the app's work without a window
|
||||
|
||||
Both use the same shares, library, settings and keyring, so a share added with `pstr` shows up in
|
||||
the app.
|
||||
|
||||
## proton-stream
|
||||
|
||||
```bash
|
||||
proton-stream # open the app
|
||||
proton-stream 'https://drive.proton.me/urls/TOKEN#KEY' # open it with the Add form filled in
|
||||
```
|
||||
|
||||
Only one window runs at a time. A second `proton-stream` brings the open window forward, hands it
|
||||
any share link it was given, and exits.
|
||||
|
||||
A link given as an argument is visible to other users of the computer in the process list while
|
||||
the command runs, and it may land in your shell history. Pasting it into the window with
|
||||
**Ctrl+V** does neither.
|
||||
|
||||
## pstr
|
||||
|
||||
`pstr --help` lists every command, and `pstr <command> --help` explains one. `pstr` never takes a
|
||||
password or key as an argument; it asks for them.
|
||||
|
||||
### Shares
|
||||
|
||||
| Command | Does |
|
||||
|---|---|
|
||||
| `pstr add --name NAME URL` | Add a public share link. Quote the URL, because of the `#`. |
|
||||
| `pstr add --name NAME URL --custom-password` | The same, and ask for the link's own password |
|
||||
| `pstr add-folder --name NAME VOLUME:LINK` | Add a folder of your Drive, by the id `pstr browse` prints |
|
||||
| `pstr shares` | List the shares and their ids |
|
||||
| `pstr remove ID` | Remove a share and delete its link and password from the keyring |
|
||||
|
||||
### Account
|
||||
|
||||
| Command | Does |
|
||||
|---|---|
|
||||
| `pstr login ADDRESS` | Sign in. Asks for the password, a two-factor code and a mailbox password as the account needs them. |
|
||||
| `pstr logout` | Sign out and delete the stored session |
|
||||
| `pstr account` | Show who is signed in |
|
||||
| `pstr browse [VOLUME:LINK]` | List your Drive's top-level places, or one folder |
|
||||
| `pstr sync` | Sync watch history now |
|
||||
|
||||
`pstr login` cannot show a CAPTCHA. If Proton asks for one, sign in once in the app instead.
|
||||
|
||||
### Library
|
||||
|
||||
| Command | Does |
|
||||
|---|---|
|
||||
| `pstr crawl [--share ID]` | Read every share, or one, into the library |
|
||||
| `pstr list [--share ID]` | Print what the library holds |
|
||||
|
||||
### Posters and descriptions
|
||||
|
||||
| Command | Does |
|
||||
|---|---|
|
||||
| `pstr metadata status` | Show whether lookups are on, and which provider |
|
||||
| `pstr metadata set --on --provider anilist` | Turn lookups on with AniList |
|
||||
| `pstr metadata set --on --provider tmdb --api-key` | Turn lookups on with TMDB, and ask for the key |
|
||||
| `pstr metadata set --off` | Turn lookups off, and delete every stored answer |
|
||||
| `pstr metadata match [--force] [--limit N]` | Look up titles now. `--force` asks again about titles that already have an answer. |
|
||||
|
||||
### Testing playback
|
||||
|
||||
These two are for finding out which layer is slow or broken.
|
||||
|
||||
```bash
|
||||
pstr bench --file Frieren --verify # measure seek time, throughput and memory, no player
|
||||
pstr play --file Frieren --disk-cache # play in a plain mpv window
|
||||
```
|
||||
|
||||
| Option | Does |
|
||||
|---|---|
|
||||
| `--file TEXT` | The first file whose name or title contains `TEXT` |
|
||||
| `--share ID` | Look in one share only |
|
||||
| `--disk-cache` | Keep decrypted blocks on disk, as the app does |
|
||||
| `--readahead N` | Blocks to fetch ahead of the reader |
|
||||
| `--ring-mib N` | Memory for blocks, in MiB (128 by default) |
|
||||
| `--verify` | `bench` only: compare a read against the SDK's own download |
|
||||
| `--throughput-mib N` | `bench` only: how much to read for the speed test (32 by default) |
|
||||
| `--start SECONDS` | `play` only: start at this position |
|
||||
| `--headless` | `play` only: decode with no window and no sound |
|
||||
| `--for-seconds S` | `play` only: stop after this many seconds |
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Does |
|
||||
|---|---|
|
||||
| `RUST_LOG` | How much both programs log to the terminal, for example `RUST_LOG=debug` |
|
||||
| `PSTR_FRAME_CAP` | The app's frame rate limit on Wayland. 60 by default; `0` turns it off. |
|
||||
@@ -0,0 +1,63 @@
|
||||
# FAQ
|
||||
|
||||
## Is this an official Proton app?
|
||||
|
||||
No. It is an unofficial, open-source client written by one developer. Proton AG has not reviewed,
|
||||
endorsed or supported it. "Proton" and "Proton Drive" are Proton AG's trademarks.
|
||||
|
||||
## Do I need a Proton account?
|
||||
|
||||
No, not to watch a public share link. You need one to add folders of your own Drive and to sync
|
||||
your place across devices. See [Account and sync](ACCOUNT_AND_SYNC.md).
|
||||
|
||||
## Does it download the whole file before playing?
|
||||
|
||||
No. Proton Drive stores a file as blocks that each decrypt on their own, so the app fetches only
|
||||
the blocks around the point you are watching. A seek fetches the block it lands on. On a
|
||||
26-minute 1080p episode the first frame takes about 2.3 seconds.
|
||||
|
||||
## Does it transcode?
|
||||
|
||||
No. The video plays as it is stored, through libmpv. HEVC 10-bit, HDR and ASS subtitles all work,
|
||||
with hardware decoding where the device has it.
|
||||
|
||||
## Does it provide any videos?
|
||||
|
||||
No. It plays what you keep in Proton Drive or what someone shares with you. It has no catalogue,
|
||||
no search of other people's shares and no content of its own.
|
||||
|
||||
## Why are there no posters?
|
||||
|
||||
Posters and descriptions are off by default, because looking them up sends your title names to
|
||||
AniList or TMDB. Turn them on in **Settings → Posters and descriptions**. See
|
||||
[Privacy](PRIVACY.md).
|
||||
|
||||
## A title is matched to the wrong show. How do I fix it?
|
||||
|
||||
Open the title and choose **Match…** (or **Change match**). Search for the right entry and pick
|
||||
it. A match you pick is never replaced, and it syncs to your other devices.
|
||||
|
||||
## The episodes of one show appear as separate titles.
|
||||
|
||||
The app takes the show's name from its folder. Put every episode of a show in one folder, or in
|
||||
season folders below it. [How the library reads your
|
||||
folders](USER_GUIDE.md#how-the-library-reads-your-folders) has examples.
|
||||
|
||||
## Can several people share one library?
|
||||
|
||||
Yes. Send them the same share link. Each person's app keeps its own watch history; sync only
|
||||
connects devices that sign in to the same Proton account.
|
||||
|
||||
## Is there a macOS version?
|
||||
|
||||
The packaging scripts exist, but no build has been run on a Mac yet, so there is no release for
|
||||
it.
|
||||
|
||||
## Can I cast to a TV?
|
||||
|
||||
Not yet. On an Android TV, install the APK and run the app on the TV itself.
|
||||
|
||||
## Where do I report a bug?
|
||||
|
||||
Open an issue on [GitHub](https://github.com/narrrl/proton-stream/issues). Say which platform
|
||||
and version you use, and what you did.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Getting started
|
||||
|
||||
This page takes you from nothing to a playing episode. It takes a few minutes.
|
||||
|
||||
## 1. Install the app
|
||||
|
||||
Download the package for your system from the
|
||||
[latest release](https://github.com/narrrl/proton-stream/releases/latest) and install it.
|
||||
[Installation](INSTALL.md) has the commands for each platform.
|
||||
|
||||
## 2. Add a share
|
||||
|
||||
A *share* is a folder the app reads videos from. There are two kinds:
|
||||
|
||||
- **A public share link.** Someone sends you a Proton Drive link such as
|
||||
`https://drive.proton.me/urls/ABC123#xyz`. You need no Proton account for this.
|
||||
- **A folder of your own Drive.** Sign in to your Proton account, then pick a folder. See
|
||||
[Account and sync](ACCOUNT_AND_SYNC.md).
|
||||
|
||||
To add a public link on the desktop:
|
||||
|
||||
1. Copy the whole link, including the part after `#`. That part is the key that decrypts the
|
||||
share, so the link does not work without it.
|
||||
2. Open the **Shares** page, or press **Ctrl+V** anywhere outside a text field.
|
||||
3. Paste the link into **Link**.
|
||||
4. If the person who shared it set a password of their own, turn on **The link asks for a
|
||||
password** and type it.
|
||||
5. Click **Add share**.
|
||||
|
||||
The app opens the link to check it before it saves anything. If the link is wrong, it tells you
|
||||
why and stores nothing.
|
||||
|
||||
On Android and iOS, tap **Add share** on the **Shares** tab. On Android you can also share the
|
||||
link to the app from your browser. [Android and iOS](MOBILE.md) has the details.
|
||||
|
||||
## 3. Wait for the library to fill
|
||||
|
||||
The app reads the share's folders. This is called a *crawl*. Titles appear in the **Library** as
|
||||
they are found. A large share takes a minute or two.
|
||||
|
||||
The folder layout decides how titles are grouped. Each show should have its own folder:
|
||||
|
||||
```text
|
||||
Sousou no Frieren/
|
||||
Season 01/
|
||||
S01E01.mkv
|
||||
S01E02.mkv
|
||||
Paprika (2006).mkv
|
||||
```
|
||||
|
||||
A file without episode numbers is shown as a film. [How the library reads your
|
||||
folders](USER_GUIDE.md#how-the-library-reads-your-folders) has the full rules.
|
||||
|
||||
## 4. Press play
|
||||
|
||||
Open a title and click **Play**. The episode starts in a few seconds, and you can drag the seek
|
||||
bar to any point. The app remembers where you stopped, so **Resume** takes you back.
|
||||
|
||||
## Next steps
|
||||
|
||||
- **Posters and descriptions.** Turn them on in **Settings → Posters and descriptions**. They are
|
||||
off by default, because the app then sends your title names to AniList or TMDB. See
|
||||
[Privacy](PRIVACY.md).
|
||||
- **Your place on every device.** Sign in to Proton on each device, and your position in every
|
||||
episode follows you. See [Account and sync](ACCOUNT_AND_SYNC.md).
|
||||
- **Keyboard.** Press **?** on the desktop for the list of shortcuts.
|
||||
@@ -0,0 +1,123 @@
|
||||
# Installation
|
||||
|
||||
Every [release](https://github.com/narrrl/proton-stream/releases/latest) has packages for Linux
|
||||
and Windows, an APK for Android and an IPA for iOS, plus a `SHA256SUMS` file to check them
|
||||
against. The version is part of each file name.
|
||||
|
||||
| Platform | File | Needs |
|
||||
|---|---|---|
|
||||
| Debian, Ubuntu | `proton-stream_<version>-1_amd64.deb` | x86_64 |
|
||||
| Fedora | `proton-stream-<version>-1.x86_64.rpm` | x86_64 |
|
||||
| Other Linux | `proton-stream-<version>-x86_64-unknown-linux-gnu.tar.gz` | x86_64, libmpv 2 |
|
||||
| Arch Linux | Built from the repository with `makepkg` | |
|
||||
| Windows | `proton-stream-<version>-x64.msi` or `…-x86_64-windows.zip` | x64 |
|
||||
| Android, Android TV | `proton-stream-<version>-android.apk` | Android 12 or newer, arm64 or x86_64 |
|
||||
| iOS, iPadOS | `proton-stream-<version>-ios.ipa` | iOS 17 or newer, SideStore or AltStore |
|
||||
|
||||
## Linux
|
||||
|
||||
The desktop app needs libmpv, WebKitGTK 4.1 (for the CAPTCHA window when you sign in) and a
|
||||
Secret Service keyring such as GNOME Keyring or KWallet. The packages pull in everything except
|
||||
the keyring, which most desktops already have.
|
||||
|
||||
### Debian and Ubuntu
|
||||
|
||||
Download the `.deb`, then:
|
||||
|
||||
```bash
|
||||
sudo apt install ./proton-stream_*_amd64.deb
|
||||
```
|
||||
|
||||
### Fedora
|
||||
|
||||
Download the `.rpm`, then:
|
||||
|
||||
```bash
|
||||
sudo dnf install ./proton-stream-*.x86_64.rpm
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
|
||||
The `PKGBUILD` in `packaging/` builds the checkout it sits in. It needs network access, so cargo
|
||||
can fetch crates:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/narrrl/proton-stream.git
|
||||
cd proton-stream/packaging
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
### Other distributions
|
||||
|
||||
Unpack the tarball anywhere. It holds `proton-stream` (the app), `pstr` (the
|
||||
[command line](CLI.md)) and the icon. Install libmpv 2 and WebKitGTK 4.1 from your
|
||||
distribution first.
|
||||
|
||||
Both packages install:
|
||||
|
||||
- `/usr/bin/proton-stream`, the app, listed as **Proton Stream** in your applications menu
|
||||
- `/usr/bin/pstr`, the command line
|
||||
|
||||
## Windows
|
||||
|
||||
Download either file from the release:
|
||||
|
||||
- **The `.msi` installer** installs to `Program Files\Proton Stream`, adds a Start menu entry
|
||||
and puts `pstr` on the PATH.
|
||||
- **The portable `.zip`** runs from any folder. Keep every file in it together: the app does not
|
||||
start without `libmpv-2.dll` and `WebView2Loader.dll` beside it.
|
||||
|
||||
The binaries are not code-signed yet, so SmartScreen may warn the first time. Choose **More
|
||||
info**, then **Run anyway**. The sign-in window uses the WebView2 runtime that comes with Windows
|
||||
10 and 11.
|
||||
|
||||
## Android and Android TV
|
||||
|
||||
1. Download `proton-stream-<version>-android.apk` on the device.
|
||||
2. Open it. Android asks once to allow installs from your browser or file manager.
|
||||
3. Install.
|
||||
|
||||
The APK is signed, so later versions install over it and keep your library. On Android TV the app
|
||||
appears in the launcher; installing needs a file manager or `adb install`.
|
||||
|
||||
## iOS and iPadOS
|
||||
|
||||
The `.ipa` is unsigned. [SideStore](https://sidestore.io) or [AltStore](https://altstore.io)
|
||||
signs it with your own Apple ID when it installs:
|
||||
|
||||
1. Install SideStore or AltStore by following its own guide.
|
||||
2. Download `proton-stream-<version>-ios.ipa`.
|
||||
3. Open it with SideStore or AltStore.
|
||||
|
||||
A free Apple ID has to refresh the app every 7 days. SideStore and AltStore do that for you.
|
||||
|
||||
## Updating
|
||||
|
||||
Install the new release over the old one. Your shares, library, watch history and settings stay.
|
||||
The app does not check for updates by itself; watch the
|
||||
[releases](https://github.com/narrrl/proton-stream/releases) on GitHub.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
Remove the package the way you installed it, for example `sudo apt remove proton-stream`.
|
||||
This leaves your data behind. To remove that too:
|
||||
|
||||
1. In the app, remove each share on the **Shares** page. This deletes its link and password
|
||||
from the keyring and its downloads from the disk.
|
||||
2. Sign out on the **Shares** page, if you signed in.
|
||||
3. Delete the app's folders, listed in [Privacy](PRIVACY.md#what-stays-on-your-device).
|
||||
|
||||
On Android and iOS, uninstalling the app deletes everything it stored.
|
||||
|
||||
## Building from source
|
||||
|
||||
Rust 1.96 or newer, plus the libmpv (≥ 2.0) development headers, and WebKitGTK 4.1's on Linux:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/narrrl/proton-stream.git
|
||||
cd proton-stream
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
[Development](DEVELOPMENT.md) and `packaging/README.md` have the details, including Windows, the
|
||||
Android toolchain and iOS builds on macOS.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Android and iOS
|
||||
|
||||
The Android and iOS apps share the desktop app's engine, so a library, its watch history and its
|
||||
settings behave the same everywhere. This page covers what is different on a phone, a tablet and
|
||||
a TV. [Installation](INSTALL.md) explains how to install each one.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="../images/android-home.png" alt="Home" width="200"></td>
|
||||
<td><img src="../images/android-library.png" alt="All titles" width="200"></td>
|
||||
<td><img src="../images/android-title.png" alt="A title page" width="200"></td>
|
||||
<td><img src="../images/android-episodes.png" alt="Episodes and downloads" width="200"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Adding a share
|
||||
|
||||
Tap **Add share** on the **Shares** tab, paste the link, and add a custom password if the link has
|
||||
one. The app checks the link before it saves it.
|
||||
|
||||
- **Android:** you can also share a link to the app from your browser or a chat app, or open a
|
||||
`drive.proton.me/urls/…` link directly if you allow the app to open supported links in the
|
||||
system settings.
|
||||
- **iOS:** there is no share sheet. Copy the link, then tap **Paste link** in the dialog.
|
||||
|
||||
Folders of your own Drive work as on the desktop: sign in on the **Shares** tab, then **Add from
|
||||
your Drive**. See [Account and sync](ACCOUNT_AND_SYNC.md).
|
||||
|
||||
## The player
|
||||
|
||||
- Tap to show or hide the controls.
|
||||
- **Double-tap** the left or right third of the screen to go back or forward 10 seconds. Double-tap
|
||||
the middle to pause.
|
||||
- The menu has **Episodes**, **Chapters**, **Playback speed**, the audio and subtitle tracks, and
|
||||
**Lock to landscape**.
|
||||
- Sound keeps playing with the screen off or the app in the background, with controls on the lock
|
||||
screen. Turn off **Background audio** in Settings to stop that.
|
||||
|
||||
Picture-in-Picture is not available on either platform.
|
||||
|
||||
## Android TV
|
||||
|
||||
The app appears in the TV launcher. With the controls hidden:
|
||||
|
||||
| Remote | Does |
|
||||
|---|---|
|
||||
| Left / right | Back or forward 10 seconds |
|
||||
| Centre, up or down | Show the controls |
|
||||
| Play/pause | Pause and play |
|
||||
|
||||
There is no separate TV layout yet, and the app has not been tested on a real TV. Reports are
|
||||
welcome.
|
||||
|
||||
## Downloads
|
||||
|
||||
Download an episode, a season or a show from its title page, and play it from the **Downloads**
|
||||
tab without a connection.
|
||||
|
||||
- **Android** waits for Wi-Fi unless you turn off **Download on Wi-Fi only**. Downloads continue
|
||||
with the app closed.
|
||||
- **iOS** downloads only while the app is open, and for the short time iOS allows after you leave
|
||||
it. A stopped download resumes where it stopped.
|
||||
|
||||
**Delete all offline episodes** in Settings frees the space in one step.
|
||||
|
||||
## New episodes
|
||||
|
||||
When a share gains episodes of a show, the app sends a notification.
|
||||
|
||||
- **Android** checks about every 6 hours, when the device has a network and the battery is not
|
||||
low. Turn it off with **New episode notifications** in Settings.
|
||||
- **iOS** asks the system to check every 6 hours or so; iOS decides when it actually runs.
|
||||
|
||||
## Where your secrets are kept
|
||||
|
||||
- **Android:** in the app's private storage, encrypted with a key in the Android Keystore. If you
|
||||
change or remove the device's screen lock, Android may delete that key. The app then asks you
|
||||
to **Re-enter link** for each share.
|
||||
- **iOS:** in the Keychain, only on this device, and not in backups.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Android 12** or newer, on an arm64 or x86_64 device. 32-bit devices are not supported.
|
||||
- **iOS 17** or newer, on iPhone or iPad.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Privacy
|
||||
|
||||
proton-stream has no server and no account with its developer. Each app talks to Proton directly,
|
||||
and to nothing else unless you turn on posters and descriptions. This page lists what goes where.
|
||||
|
||||
## What the app sends, and to whom
|
||||
|
||||
| To | What | When |
|
||||
|---|---|---|
|
||||
| Proton | Requests for the shares you added and, if you signed in, your account's Drive | Always |
|
||||
| AniList and ani.zip, or TMDB | The names of the titles in your library, and the ids of matched titles | Only with **Posters and descriptions** on |
|
||||
|
||||
Nothing else leaves your device. There is no telemetry, no crash reporting and no update check.
|
||||
|
||||
**Posters and descriptions** are off by default. When they are on, the provider learns the names
|
||||
of the titles in your library. It does not learn your files, your watch history or your shares.
|
||||
Turning them off deletes every stored answer.
|
||||
|
||||
## Secrets
|
||||
|
||||
- **The part of a share link after `#`** is the key that decrypts the share. The app treats it as
|
||||
a password.
|
||||
- **A link's own password**, if it has one.
|
||||
- **Your Proton session**, if you signed in. The app keeps the session, not your password.
|
||||
|
||||
All of these go into the system's credential store, never into a config file:
|
||||
|
||||
| Platform | Store |
|
||||
|---|---|
|
||||
| Linux | Secret Service (GNOME Keyring, KWallet, KeePassXC) |
|
||||
| Windows | Credential Manager |
|
||||
| Android | App storage, encrypted with a key in the Android Keystore |
|
||||
| iOS | Keychain, this device only |
|
||||
|
||||
If you sign in, your shares, with their links and passwords, also sync through your own Drive.
|
||||
Like every file there, they are end-to-end encrypted. See [Account and sync](ACCOUNT_AND_SYNC.md).
|
||||
|
||||
## How the player gets the video
|
||||
|
||||
The player is fed through a callback inside the app's own process. There is no local web server,
|
||||
so there is no port another program on the computer could ask for the decrypted video. The
|
||||
player is handed an opaque token, not a link, so no secret ends up in its log or window title.
|
||||
|
||||
## What stays on your device
|
||||
|
||||
| What | Where on Linux |
|
||||
|---|---|
|
||||
| Settings and the share list (without secrets) | `~/.config/proton-stream/` |
|
||||
| The library and watch history (`catalog.db`) and downloads (`offline/`) | `~/.local/share/proton-stream/` |
|
||||
| Streamed video (`blocks/`), thumbnails and posters | `~/.cache/proton-stream/` |
|
||||
|
||||
On Windows the same folders are under `%APPDATA%\narl\proton-stream\` and
|
||||
`%LOCALAPPDATA%\narl\proton-stream\`. On macOS they are under `~/Library/Application Support/`
|
||||
and `~/Library/Caches/`, in `io.narl.proton-stream`.
|
||||
|
||||
**The streaming cache and downloads hold decrypted video.** Anyone who can read your user's files
|
||||
can play them. Empty the cache in **Settings → Storage**, and delete downloads on the
|
||||
**Downloads** page.
|
||||
|
||||
The library database holds file names and your watch history, in plain text. It holds no
|
||||
secrets.
|
||||
|
||||
## Other people on the same computer
|
||||
|
||||
- A share link given on the command line, as in `proton-stream 'https://…#key'`, is visible in
|
||||
the process list while the command runs, and may land in your shell history. Paste links into
|
||||
the window instead.
|
||||
- Your desktop's media controls are given the title, the episode and a picture already on disk.
|
||||
They are never given a provider link, so the desktop does not contact a provider by itself.
|
||||
@@ -0,0 +1,41 @@
|
||||
# proton-stream documentation
|
||||
|
||||
proton-stream turns the videos in your Proton Drive, and the share links people send you, into
|
||||
a library you can browse and stream. It runs on Linux, Windows, Android, Android TV and iOS.
|
||||
This is an unofficial client, not a Proton product.
|
||||
|
||||
<img src="../images/desktop-home.png" alt="The library in the desktop app" width="820">
|
||||
|
||||
## Start here
|
||||
|
||||
- [Getting started](GETTING_STARTED.md): install, add your first share, press play.
|
||||
- [Installation](INSTALL.md): every platform, and how to update and uninstall.
|
||||
- [FAQ](FAQ.md): short answers to the questions people ask first.
|
||||
|
||||
## Using it
|
||||
|
||||
- [User guide](USER_GUIDE.md): the library, title pages, the player, downloads, history and
|
||||
settings.
|
||||
- [Account and sync](ACCOUNT_AND_SYNC.md): signing in, folders of your own Drive, and keeping
|
||||
your place on every device.
|
||||
- [Android and iOS](MOBILE.md): what is different on a phone, a tablet and Android TV.
|
||||
|
||||
## Reference
|
||||
|
||||
- [Command-line reference](CLI.md): the `pstr` tool and the `proton-stream` launcher.
|
||||
- [Privacy](PRIVACY.md): what goes where, and what stays on your device.
|
||||
|
||||
## When something goes wrong
|
||||
|
||||
- [Troubleshooting](TROUBLESHOOTING.md): links that will not add, sign-in problems, playback
|
||||
problems, and where the logs are.
|
||||
|
||||
## Contributing
|
||||
|
||||
- [Development](DEVELOPMENT.md): the state of each layer, the build order and the traps.
|
||||
- [Architecture](ARCHITECTURE.md): the crates, the data flow when an episode plays, and where
|
||||
secrets live.
|
||||
- [Testing](TESTING.md), [Metadata](METADATA.md), [Design tokens](DESIGN-TOKENS.md),
|
||||
[Android development](ANDROID.md) and [iOS development](IOS.md).
|
||||
|
||||
Bugs are tracked in [BUGS.md](BUGS.md) in the repository.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Adding a share
|
||||
|
||||
### "Proton share URL has no #password fragment; copy the full share link"
|
||||
|
||||
The link is missing the part after `#`, which is its decryption key. Copy the link again from
|
||||
where you got it, all of it.
|
||||
|
||||
### "This link has a password of its own. Enter it to add the link."
|
||||
|
||||
The person who shared it also set a password. Turn on **The link asks for a password** (on a
|
||||
phone, fill in **Custom password**) and type it.
|
||||
|
||||
### "That share is already added."
|
||||
|
||||
The link, or the same folder of your Drive, is already on the **Shares** page.
|
||||
|
||||
### "Last crawl failed: …"
|
||||
|
||||
The share could not be read. The link may have expired or been revoked, or its password
|
||||
changed. If the password changed, choose **Re-enter link**.
|
||||
|
||||
## The keyring
|
||||
|
||||
### "share … has no stored credentials; remove and re-add it"
|
||||
|
||||
The link and password are gone from the keyring. This happens when a keyring is reset, or on
|
||||
Android when the screen lock changes. Choose **Re-enter link** on the **Shares** page. Your
|
||||
library, history and downloads stay.
|
||||
|
||||
### "credential store: …" on Linux
|
||||
|
||||
The app found no Secret Service. Install and start one, such as GNOME Keyring, KWallet or
|
||||
KeePassXC with its Secret Service integration, and unlock it.
|
||||
|
||||
## Signing in
|
||||
|
||||
### "Proton did not accept that address and password"
|
||||
|
||||
Check both. Sign in on proton.me to make sure the account works.
|
||||
|
||||
### "Proton has blocked sign-ins from this app for now"
|
||||
|
||||
Proton's abuse protection stopped the sign-in. Wait a while and try again, or sign in once on
|
||||
proton.me first.
|
||||
|
||||
### "This account's only second factor is a security key"
|
||||
|
||||
The app cannot use security keys yet. Add an authenticator app as a second method in your
|
||||
Proton account settings, then sign in with its code.
|
||||
|
||||
### The CAPTCHA window does not open on Linux
|
||||
|
||||
The window needs WebKitGTK 4.1. The `.deb`, `.rpm` and Arch packages pull it in; with the
|
||||
tarball, install it yourself.
|
||||
|
||||
### `pstr login` asks for a CAPTCHA
|
||||
|
||||
The command line cannot show one. Sign in once in the app.
|
||||
|
||||
## Playback
|
||||
|
||||
### Green blocks, a black picture or torn frames
|
||||
|
||||
Turn off **Hardware decoding** in Settings. It applies from the next file.
|
||||
|
||||
### An episode takes long to start
|
||||
|
||||
The first seconds of a file are fetched over the network. On a slow connection, start the
|
||||
episode and give it a moment, or download it first. A second viewing comes from the disk cache.
|
||||
|
||||
### No skip button for the opening
|
||||
|
||||
The skip button comes from the file's chapter names. A file without named chapters gets none.
|
||||
|
||||
### The wrong subtitle track plays
|
||||
|
||||
Choose the track in the player. The app remembers it for the rest of the show. Set the
|
||||
language to start in under **Settings → Playback**.
|
||||
|
||||
## Windows
|
||||
|
||||
### The app does not start from the portable `.zip`
|
||||
|
||||
Keep every file from the archive in one folder. The app needs `libmpv-2.dll` and
|
||||
`WebView2Loader.dll` next to `proton-stream.exe`.
|
||||
|
||||
### SmartScreen warns about the installer
|
||||
|
||||
The binaries are not code-signed yet. Choose **More info**, then **Run anyway**.
|
||||
|
||||
### A pinned taskbar icon shows a blank picture after an update
|
||||
|
||||
Unpin it and pin it again once.
|
||||
|
||||
## Linux
|
||||
|
||||
### "Not responding" when the window is on another workspace
|
||||
|
||||
Fixed. On Wayland the app limits its own frame rate instead of waiting for vsync, which a
|
||||
compositor withholds from a window it is not showing. Update to the latest release.
|
||||
|
||||
## Logs
|
||||
|
||||
Start the app from a terminal to see its log:
|
||||
|
||||
```bash
|
||||
RUST_LOG=debug proton-stream
|
||||
```
|
||||
|
||||
The same works for `pstr`. When you
|
||||
[open an issue](https://github.com/narrrl/proton-stream/issues), include the lines around the
|
||||
error, but **remove any share link first**: the part after `#` is a key.
|
||||
@@ -0,0 +1,198 @@
|
||||
# User guide
|
||||
|
||||
This page describes the desktop app. The Android and iOS apps have the same pages as tabs at the
|
||||
bottom of the screen; [Android and iOS](MOBILE.md) covers what is different there.
|
||||
|
||||
The app has five pages, in the bar at the top: **Library**, **History**, **Shares**,
|
||||
**Downloads** and **Settings**. **Ctrl+1** to **Ctrl+5** open them.
|
||||
|
||||
## Library
|
||||
|
||||
<img src="../images/desktop-library.png" alt="The whole library, filtered and sorted" width="820">
|
||||
|
||||
The library shows every title from every share in one place. Two shares that hold the same show
|
||||
show it once.
|
||||
|
||||
- **The banner** at the top shows the title you watched last, with **Resume** and **More info**.
|
||||
- **Continue watching** has the episode you stopped in for each show, or the next one when you
|
||||
finished an episode. To take a show off this row, right-click it and choose **Remove from
|
||||
Continue watching**.
|
||||
- **Shelves** such as **Airing now**, **Directed by Satoshi Kon** or **From MADHOUSE** appear when
|
||||
posters and descriptions are on. The app shows at most four, and only for a director, studio or
|
||||
genre with at least three titles.
|
||||
- **All titles** is the full grid. Choose a filter and a sort order above it.
|
||||
|
||||
| Filters | Sort orders |
|
||||
|---|---|
|
||||
| All, Series, Films, Watching, Not started, Downloaded | A – Z, Recently watched, Recently added, Newest, Highest rated, Most popular |
|
||||
|
||||
**Group franchises** folds a series, its sequels and its films into one tile with a **+N** badge.
|
||||
It is on by default, and needs posters and descriptions to know what belongs together.
|
||||
|
||||
**Search** with **Ctrl+F** or **/**. It matches every name a title has: the English one, the
|
||||
romaji one and the name of its folder.
|
||||
|
||||
**Refresh** (or **F5**) reads every share again. The app also does this by itself when a share
|
||||
has not been read for 6 hours.
|
||||
|
||||
### How the library reads your folders
|
||||
|
||||
The folder decides which show a file belongs to. The file name gives the season and episode.
|
||||
|
||||
| Layout | Read as |
|
||||
|---|---|
|
||||
| `Frieren/Season 01/S01E05.mkv` | Frieren, season 1, episode 5 |
|
||||
| `Frieren/S01E05 - The Hero's Statue.mkv` | The same, with the episode title |
|
||||
| `Bocchi the Rock/[Group] Bocchi the Rock - 07 [1080p].mkv` | Bocchi the Rock, episode 7 |
|
||||
| `Spy x Family/Season 2 - 06.mkv` | Spy x Family, season 2, episode 6 |
|
||||
| `Akira (1988).mkv` | A film, from 1988 |
|
||||
|
||||
- Season folders can be called `Season 01`, `Season 1`, `S01` or `3rd Season`.
|
||||
- Folders such as `Specials`, `Extras`, `OVA`, `NCOP` or `Subs` are not taken as a show's name;
|
||||
the folder above them is.
|
||||
- An episode title is read only after a dash, as in `S01E01 - Name`.
|
||||
- A file with no episode number is a film.
|
||||
|
||||
The app plays `mkv`, `mp4`, `m4v`, `avi`, `mov`, `webm`, `ts`, `m2ts` and most other common video
|
||||
containers.
|
||||
|
||||
## A title's page
|
||||
|
||||
Click a title to open its page. **Esc** goes back.
|
||||
|
||||
- **Resume S01E03 · 12:04** continues where you stopped. **Start over** plays the episode from
|
||||
the beginning.
|
||||
- **Download show** keeps every episode on this device. See [Downloads](#downloads).
|
||||
- **Match…** opens the matcher, where you choose which AniList or TMDB entry the title is. Use it
|
||||
when the app guessed wrong or did not guess at all. A match you pick is never replaced.
|
||||
- **More on AniList** (or TMDB) opens the title's page there.
|
||||
- Each season has **Mark season watched** and **Download season**.
|
||||
- Each episode shows a tick when you have watched it. Right-click an episode and choose **Mark
|
||||
watched up to here** to tick it and every episode before it.
|
||||
- **In this franchise** lists the other parts of the series, when posters and descriptions are on.
|
||||
|
||||
An episode counts as watched once you have seen 90% of it.
|
||||
|
||||
## The player
|
||||
|
||||
The picture plays inside the app's window. Move the mouse to show the controls.
|
||||
|
||||
- **Skip opening** and **Skip ending** appear when the file's chapters mark an opening, an ending
|
||||
or a preview. Files without chapter names get no skip button. Turn on **Skip openings and
|
||||
credits** in Settings to skip them without asking.
|
||||
- **Up next** counts down 10 seconds when the credits start, then plays the next episode. Choose
|
||||
**Play now**, **Cancel** or **Watch till the end**.
|
||||
- **Subtitles and audio** are chosen from the buttons at the bottom right. The app remembers your
|
||||
choice for each show.
|
||||
- **Speed** goes from 0.5× to 3×, and stays for the next episode.
|
||||
|
||||
### Keyboard
|
||||
|
||||
Press **?** anywhere to see this list in the app.
|
||||
|
||||
| Key | Anywhere |
|
||||
|---|---|
|
||||
| Ctrl+F or / | Search the library |
|
||||
| F5 or Ctrl+R | Read every share again |
|
||||
| Ctrl+1 – 5 | Library, History, Shares, Downloads, Settings |
|
||||
| Ctrl+, | Settings |
|
||||
| Ctrl+V | Paste a share link to add it |
|
||||
| Tab or arrows | Move between titles and episodes |
|
||||
| Enter | Open or play what is highlighted |
|
||||
| Alt+← or mouse back | Back |
|
||||
|
||||
| Key | Playing |
|
||||
|---|---|
|
||||
| Space or K | Pause and play |
|
||||
| ← / → | Back 10 s, forward 30 s |
|
||||
| ↑ / ↓ | Volume |
|
||||
| M | Mute |
|
||||
| N / P | Next and previous episode |
|
||||
| [ / ] | Slower and faster |
|
||||
| S | Skip the opening or the credits |
|
||||
| E | The list of episodes |
|
||||
| F or double-click | Fullscreen |
|
||||
| Ctrl+S | Save the frame to `Pictures/proton-stream` |
|
||||
| Esc | Leave fullscreen, then the player |
|
||||
|
||||
Media keys and your desktop's media controls work too.
|
||||
|
||||
### Subtitle files beside the video
|
||||
|
||||
The app picks up `.srt`, `.ass`, `.ssa` and `.vtt` files next to a video:
|
||||
|
||||
```text
|
||||
Frieren/Season 01/
|
||||
S01E01.mkv
|
||||
S01E01.en.srt English
|
||||
S01E01.en.forced.ass English, "forced"
|
||||
Subs/
|
||||
S01E01/
|
||||
2_English.srt English
|
||||
```
|
||||
|
||||
A `Subs` folder beside the videos works, and so does any subtitle in a folder that holds a single
|
||||
video. The language comes from the last word in the name that names one.
|
||||
|
||||
### Dialogue, not signs and songs
|
||||
|
||||
Many anime releases have two subtitle tracks in one language: the full dialogue, and a short
|
||||
*Signs & Songs* track that only translates on-screen text. When a file would start on the short
|
||||
track, the app switches to the full one in the same language.
|
||||
|
||||
## History
|
||||
|
||||
Everything you played, newest first, grouped by day. An entry says **Watched**, **Stopped at
|
||||
12:04 of 24:00** or **Played**. Removing an entry marks a watched episode unwatched, or forgets
|
||||
where you stopped in one you had not finished.
|
||||
|
||||
## Downloads
|
||||
|
||||
A download keeps a full copy of an episode on this device, to play without a connection. Start
|
||||
one from a title's page: a whole show, a season or a single episode.
|
||||
|
||||
The **Downloads** page lists them with **Pause**, **Resume**, **Cancel**, **Retry**, **Play** and
|
||||
**Delete**. Three run at a time. Downloads do not count against the streaming cache.
|
||||
|
||||
## Shares
|
||||
|
||||
The **Shares** page lists every share, with what it holds and when it was last read.
|
||||
|
||||
- **Crawl** reads the share again.
|
||||
- **Rename** changes the name shown in the app.
|
||||
- **Re-enter link** gives the app the link again, for a keyring that lost it or a password that
|
||||
changed. The library, history and downloads stay.
|
||||
- **Remove** takes the share's titles out of the library, deletes its downloads and removes its
|
||||
link and password from the keyring.
|
||||
|
||||
The **Proton account** section on this page is where you sign in. See
|
||||
[Account and sync](ACCOUNT_AND_SYNC.md).
|
||||
|
||||
## Settings
|
||||
|
||||
**Appearance**
|
||||
|
||||
- **Palette:** Proton (the near-black default), Catppuccin Latte, Frappé, Macchiato and Mocha, or
|
||||
Persona 5.
|
||||
- **Accent:** Mauve, Pink, Sky, Pink → Sky, Lavender, Blue, Teal, Peach or Red. **Gradient
|
||||
accent** draws it as a gradient.
|
||||
- **Text size:** 90% to 150%. This one stays on this computer.
|
||||
- **Library tiles:** posters or stills. **Title names:** as in the share, English or romaji.
|
||||
|
||||
**Playback**
|
||||
|
||||
- **Audio language** and **Subtitles:** the language to start in, or the file's own default.
|
||||
- **Play the next episode** (on by default) and **Skip openings and credits** (off).
|
||||
- **Hardware decoding** (on). Turn it off if the picture shows green blocks or stays black.
|
||||
|
||||
**Storage**
|
||||
|
||||
- **Streaming cache:** 1 to 16 GiB, 4 GiB by default. It keeps the parts of videos you streamed,
|
||||
so watching again needs no network. **Clear the streaming cache** empties it.
|
||||
|
||||
**Posters and descriptions**
|
||||
|
||||
- Off by default. When it is on, the app sends the names of your titles to the provider you pick:
|
||||
**AniList** (no key needed) or **TMDB** (needs a free API key). See [Privacy](PRIVACY.md).
|
||||
- **Match the library** looks up new titles now. **Match everything again** looks up every title,
|
||||
except those you matched by hand.
|
||||
|
After Width: | Height: | Size: 653 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="proton-stream">
|
||||
<defs>
|
||||
<linearGradient id="ps-face" x1="12" y1="12" x2="116" y2="112" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#A48BFF"/>
|
||||
<stop offset="0.55" stop-color="#7552FF"/>
|
||||
<stop offset="1" stop-color="#5B36F2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ps-glyph" x1="0" y1="36" x2="0" y2="88" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FFFFFF"/>
|
||||
<stop offset="1" stop-color="#ECE6FF"/>
|
||||
</linearGradient>
|
||||
<!-- The play mark, cut into the blocks a revision is fetched in: each one
|
||||
decrypts on its own, which is the whole reason this app can seek. -->
|
||||
<mask id="ps-cut" maskUnits="userSpaceOnUse" x="0" y="0" width="128" height="128">
|
||||
<path d="M50 41.5v41a6 6 0 0 0 9.1 5.1l34.2-20.5a6 6 0 0 0 0-10.3L59.1 36.4A6 6 0 0 0 50 41.5z" fill="#FFFFFF"/>
|
||||
<g fill="#000000">
|
||||
<rect x="63" y="0" width="5.5" height="128"/>
|
||||
<rect x="78.5" y="0" width="5.5" height="128"/>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<!-- base: a rounded tile with a darker lower edge for depth -->
|
||||
<rect x="12" y="16" width="104" height="100" rx="26" fill="#4424C0"/>
|
||||
<rect x="12" y="12" width="104" height="100" rx="26" fill="url(#ps-face)"/>
|
||||
<rect x="12.5" y="12.5" width="103" height="99" rx="25.5" stroke="#FFFFFF" stroke-opacity="0.2"/>
|
||||
|
||||
<!-- glyph shadow, then the glyph itself -->
|
||||
<g transform="translate(0 3)">
|
||||
<rect width="128" height="128" fill="#2E1596" fill-opacity="0.3" mask="url(#ps-cut)"/>
|
||||
</g>
|
||||
<rect width="128" height="128" fill="url(#ps-glyph)" mask="url(#ps-cut)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish the documentation in docs/ to the GitHub wiki.
|
||||
#
|
||||
# The pages in docs/ are the source of truth; the wiki is a generated copy for
|
||||
# people who look there first. Each run checks every link in docs/, clones the
|
||||
# wiki, replaces all of its pages with the ones listed in SECTIONS below,
|
||||
# rewrites links between them to wiki page names and every other repository
|
||||
# link to an absolute GitHub URL, copies the images they show, writes the
|
||||
# sidebar and footer, and commits. Nothing is pushed without --push.
|
||||
#
|
||||
# --check only checks the links and that every page in docs/ is listed or
|
||||
# excluded, without touching the wiki. CI runs it for every change to docs/,
|
||||
# and every push to main that passes it publishes.
|
||||
#
|
||||
# GitHub creates the wiki repository only when the first page is saved, so
|
||||
# create any page once in the web UI before the first run. WIKI_REMOTE
|
||||
# overrides the remote, which is how the workflow pushes with its token.
|
||||
#
|
||||
# usage: scripts/publish-wiki.sh [--check | --push] [WORKDIR]
|
||||
set -euo pipefail
|
||||
|
||||
repo_url="https://github.com/narrrl/proton-stream"
|
||||
wiki_remote="${WIKI_REMOTE:-git@github.com:narrrl/proton-stream.wiki.git}"
|
||||
|
||||
mode=commit
|
||||
case "${1:-}" in
|
||||
--check) mode=check; shift ;;
|
||||
--push) mode=push; shift ;;
|
||||
esac
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
workdir="${1:-$(mktemp -d)}"
|
||||
wiki="$workdir/wiki"
|
||||
|
||||
if [ "$mode" = check ]; then
|
||||
wiki="$workdir/check"
|
||||
mkdir -p "$wiki"
|
||||
elif [ -d "$wiki/.git" ]; then
|
||||
git -C "$wiki" pull --ff-only --quiet
|
||||
else
|
||||
git clone --quiet "$wiki_remote" "$wiki"
|
||||
fi
|
||||
|
||||
rev="$(git -C "$root" rev-parse --short HEAD)"
|
||||
version="$(grep -m1 '^version' "$root/Cargo.toml" | cut -d'"' -f2)"
|
||||
|
||||
python3 - "$root" "$wiki" "$repo_url" "$version" "$rev" <<'EOF'
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
root, wiki, repo_url, version, rev = sys.argv[1:]
|
||||
root = pathlib.Path(root)
|
||||
wiki = pathlib.Path(wiki)
|
||||
docs = root / "docs"
|
||||
|
||||
HOME = ("README.md", "Home")
|
||||
|
||||
# Sidebar sections: docs/ file -> wiki page name, in order.
|
||||
SECTIONS = [
|
||||
("Start here", [
|
||||
("GETTING_STARTED.md", "Getting-Started"),
|
||||
("INSTALL.md", "Installation"),
|
||||
("FAQ.md", "FAQ"),
|
||||
]),
|
||||
("Using it", [
|
||||
("USER_GUIDE.md", "User-Guide"),
|
||||
("ACCOUNT_AND_SYNC.md", "Account-and-Sync"),
|
||||
("MOBILE.md", "Android-and-iOS"),
|
||||
]),
|
||||
("Reference", [
|
||||
("CLI.md", "Command-Line-Reference"),
|
||||
("PRIVACY.md", "Privacy"),
|
||||
]),
|
||||
("When something goes wrong", [
|
||||
("TROUBLESHOOTING.md", "Troubleshooting"),
|
||||
]),
|
||||
("Contributing", [
|
||||
("DEVELOPMENT.md", "Development"),
|
||||
("ARCHITECTURE.md", "Architecture"),
|
||||
("TESTING.md", "Testing"),
|
||||
("METADATA.md", "Metadata"),
|
||||
("DESIGN-TOKENS.md", "Design-Tokens"),
|
||||
("ANDROID.md", "Android-Development"),
|
||||
("IOS.md", "iOS-Development"),
|
||||
]),
|
||||
]
|
||||
|
||||
# Too long or too internal for the wiki; links to them go to GitHub.
|
||||
EXCLUDED = {"BUGS.md"}
|
||||
|
||||
# Files outside docs/ whose links are checked but not published.
|
||||
CHECKED = ["README.md"]
|
||||
|
||||
PAGES = dict([HOME] + [page for _, pages in SECTIONS for page in pages])
|
||||
images_url = f"https://raw.githubusercontent.com/wiki/{repo_url.removeprefix('https://github.com/')}"
|
||||
|
||||
LINK = re.compile(r"(\]\(|src=\")([^)\"\s]+)([)\"])")
|
||||
FENCE = re.compile(r"^\s*(```|~~~)")
|
||||
INLINE_CODE = re.compile(r"(`[^`\n]*`)")
|
||||
HEADING = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
|
||||
EXPLICIT_ID = re.compile(r"<a\s+id=\"([^\"]+)\"")
|
||||
|
||||
errors = []
|
||||
images = set()
|
||||
|
||||
|
||||
def outside_fences(text):
|
||||
"""Yield (is_prose, chunk) so links in code blocks are left alone."""
|
||||
chunk, fenced = [], False
|
||||
for line in text.splitlines(keepends=True):
|
||||
if FENCE.match(line):
|
||||
if chunk:
|
||||
yield not fenced, "".join(chunk)
|
||||
chunk, fenced = [], not fenced
|
||||
yield False, line
|
||||
continue
|
||||
chunk.append(line)
|
||||
if chunk:
|
||||
yield not fenced, "".join(chunk)
|
||||
|
||||
|
||||
def slug(heading):
|
||||
"""GitHub's anchor for a heading."""
|
||||
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", heading)
|
||||
return re.sub(r"[^\w\- ]", "", text.lower()).replace(" ", "-")
|
||||
|
||||
|
||||
anchor_cache = {}
|
||||
|
||||
|
||||
def anchors(path):
|
||||
if path not in anchor_cache:
|
||||
found, seen = set(), {}
|
||||
for prose, chunk in outside_fences(path.read_text()):
|
||||
if not prose:
|
||||
continue
|
||||
for line in chunk.splitlines():
|
||||
found.update(EXPLICIT_ID.findall(line))
|
||||
m = HEADING.match(line)
|
||||
if not m:
|
||||
continue
|
||||
base = slug(m.group(2))
|
||||
n = seen.get(base, 0)
|
||||
seen[base] = n + 1
|
||||
found.add(base if n == 0 else f"{base}-{n}")
|
||||
anchor_cache[path] = found
|
||||
return anchor_cache[path]
|
||||
|
||||
|
||||
def rewrite(source, target):
|
||||
"""The wiki form of one link in source, recording broken links."""
|
||||
if re.match(r"^[a-z]+:", target):
|
||||
return target
|
||||
path, _, anchor = target.partition("#")
|
||||
resolved = (source.parent / path).resolve() if path else source
|
||||
where = f"{source.relative_to(root)}: {target}"
|
||||
if not resolved.is_relative_to(root) or not resolved.exists():
|
||||
errors.append(f"{where}: no such file")
|
||||
return target
|
||||
if anchor and resolved.suffix == ".md" and anchor not in anchors(resolved):
|
||||
errors.append(f"{where}: no heading #{anchor}")
|
||||
relative = resolved.relative_to(root)
|
||||
anchor = f"#{anchor}" if anchor else ""
|
||||
if not path:
|
||||
return anchor
|
||||
if relative.parent == pathlib.Path("docs") and relative.name in PAGES:
|
||||
return PAGES[relative.name] + anchor
|
||||
if relative.parts[0] == "images":
|
||||
images.add(relative)
|
||||
return f"{images_url}/{relative}"
|
||||
kind = "tree" if resolved.is_dir() else "blob"
|
||||
return f"{repo_url}/{kind}/main/{relative}{anchor}"
|
||||
|
||||
|
||||
def convert(source):
|
||||
out = []
|
||||
for prose, chunk in outside_fences(source.read_text()):
|
||||
if prose:
|
||||
# Odd pieces are `inline code`, which is left alone too.
|
||||
pieces = INLINE_CODE.split(chunk)
|
||||
pieces[::2] = [
|
||||
LINK.sub(lambda m: m.group(1) + rewrite(source, m.group(2)) + m.group(3), p)
|
||||
for p in pieces[::2]
|
||||
]
|
||||
chunk = "".join(pieces)
|
||||
out.append(chunk)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def title(source):
|
||||
return (docs / source).read_text().splitlines()[0].lstrip("# ").strip()
|
||||
|
||||
|
||||
def without_title(text):
|
||||
lines = text.splitlines(keepends=True)
|
||||
if lines and lines[0].startswith("# "):
|
||||
lines = lines[1:]
|
||||
while lines and not lines[0].strip():
|
||||
lines = lines[1:]
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
for name in sorted(p.name for p in docs.glob("*.md")):
|
||||
if name not in PAGES and name not in EXCLUDED:
|
||||
errors.append(f"docs/{name}: add it to SECTIONS or EXCLUDED in scripts/publish-wiki.sh")
|
||||
|
||||
converted = {source: convert(docs / source) for source in PAGES}
|
||||
published_images = set(images)
|
||||
for name in CHECKED:
|
||||
convert(root / name)
|
||||
|
||||
if errors:
|
||||
print("Broken documentation links:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f" {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# The wiki is generated: anything not written below is stale.
|
||||
for old in wiki.glob("*.md"):
|
||||
old.unlink()
|
||||
shutil.rmtree(wiki / "images", ignore_errors=True)
|
||||
|
||||
note = (
|
||||
"<!-- Generated from docs/ in the repository by scripts/publish-wiki.sh."
|
||||
" Edit the source there; changes made here are overwritten. -->\n\n"
|
||||
)
|
||||
for source, page in PAGES.items():
|
||||
(wiki / f"{page}.md").write_text(note + without_title(converted[source]))
|
||||
|
||||
for image in published_images:
|
||||
(wiki / image).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(root / image, wiki / image)
|
||||
|
||||
sidebar = ["**[Home](Home)**\n"]
|
||||
for section, pages in SECTIONS:
|
||||
sidebar.append(f"\n**{section}**\n\n")
|
||||
sidebar += [f"- [{title(source)}]({page})\n" for source, page in pages]
|
||||
sidebar += [
|
||||
"\n---\n\n",
|
||||
f"[Releases]({repo_url}/releases) · [Issues]({repo_url}/issues)\n",
|
||||
]
|
||||
(wiki / "_Sidebar.md").write_text(note + "".join(sidebar))
|
||||
|
||||
(wiki / "_Footer.md").write_text(
|
||||
note
|
||||
+ f"Documentation of proton-stream {version}, generated from "
|
||||
+ f"[`docs/`]({repo_url}/tree/{rev}/docs) at `{rev}`. "
|
||||
+ f"To change a page, edit it in the [repository]({repo_url}).\n"
|
||||
)
|
||||
EOF
|
||||
|
||||
if [ "$mode" = check ]; then
|
||||
echo "Documentation links are fine."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$wiki"
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "Wiki is already up to date."
|
||||
exit 0
|
||||
fi
|
||||
git commit --quiet -m "Update from docs at $rev"
|
||||
git show --stat --format='%s' HEAD
|
||||
|
||||
if [ "$mode" = push ]; then
|
||||
git push --quiet
|
||||
echo "Pushed to the wiki."
|
||||
else
|
||||
echo "Committed in $wiki. Review it, then push from there or re-run with --push."
|
||||
fi
|
||||