A crawl now keeps .srt/.ass/.ssa/.vtt files and pairs each with a video by
name: the video's name followed by a dot or nothing, the same inside a
Subs folder, the Subs/<video>/ layout, and failing those the only video in
the folder. The words past the video's name give the language and the
picker's title. They go in a new sidecars table (schema v12), rebuilt by
every crawl.
The desktop opens them while mpv opens the video and adds them with
sub-add through the stream registry once the file has loaded. One in the
preferred language is turned on when subtitles are on and the file has
none of its own playing; the rest wait in the subtitle menu.
A transport failure's text is reqwest's "error sending request for url
(…)", and the cause sits further down its source chain. Error::describe
appends the causes and drops the URL, so the reason fits on a share row.
Resuming loaded the file from the start and sought on FileLoaded. By
then mpv's demuxer had begun reading ahead from the beginning, so the
blocks the seek wanted queued behind ones fetched to be thrown away.
Player::play_from sets mpv's start option instead, and puts it back
once the file has loaded.
VideoStream::warm_resume fetches, beside the two edge blocks, the block
the resume position falls in, estimated from the running time saved
with the watch state. A video's bitrate is uneven, so it can land a
block off; then it is one block for nothing, against a round trip saved
when it lands.
Settings > Appearance offers 90% to 150%, applied as egui's zoom factor
so controls grow with the text. Ctrl+plus and Ctrl+minus already zoomed
through egui; what they settle on is now kept too, so Settings shows it
and the next launch opens at it. It lives in the desktop-only settings:
what reads well depends on the screen, not on the viewer's other
devices.
eframe now builds with its accesskit feature, so the widget tree reaches
the platform screen reader. Buttons drawn as an icon glyph were read as
the icon font's private code point, and the hand-painted ones (play,
poster tiles, the watched mark, download rings, the seek bar) as
nothing. A Named trait gives them a name, and hint() makes the existing
hover text that name.
A position save every five seconds marked the whole library view stale,
which re-grouped franchises, re-picked shelves and re-arranged the grid
on every save. Only the history and Continue watching lists move then,
unless the grid itself is filtered or sorted by watching.
ShareStore::rename changes only the name; the id that the catalog, watch
state and offline files are keyed by stays the same. Sync carries the
change as it does any other change to the share's register. The desktop
renames in the share's row. The phones rename from the share's menu.
Before, the name given at add time was permanent short of removing the
share.
The phones have had this, and the desktop could only set languages from
the player, after a file had already opened in the wrong one. Settings
now has an audio language and a subtitle choice: off, the file's
default, or a language. The names come from the list pstr-player already
names tracks with, now exported as LANGUAGES.
The library filter moves from the desktop into pstr_core::browse and
gains a Downloaded filter: titles with at least one episode kept on the
device. The bridge's arrangement() takes the filter. Android and iOS get
a row of filter chips above the grid. The choice is remembered per
device, and shelves stay hidden under a filter, as on the desktop.
- Turning metadata off or switching provider forgets every stored answer,
including matches chosen by hand. The desktop now asks first and says
how many hand-picked matches go. The phones' settings dialog says so
above Save.
- Forgetting a hand-picked match on the desktop, and any match on the
phones, is asked about first.
- Downloading a season, a show, or everything from a library tile asks
first, with the episode count and their size.
- Android confirmations that delete something use an error-toned text
button rather than the filled accent, which read as the safe choice.
- A link with a password of its own, added without one, now fails as
"This link has a password of its own" instead of the handshake's
refusal. The link's public flags are only asked for after a refusal.
The desktop form turns its password toggle on.
- After the first share is added, all three clients go to the library.
While that share is being listed, the empty library says so instead of
offering to add a share.
- The desktop library offers posters and synopses once there is a
library to put them on. The offer says what is sent, links to
Settings, never turns anything on, and "Not now" is remembered.
The click used to leave the viewer on the title page with a small line in
the bottom bar until the file had opened, which over a public link can be
seconds. The player page now comes up at once with the title's art dimmed
behind the episode's name and a spinner, and Back still works. A file that
fails to open goes back to the page it was asked from.
Filter, sort and franchise folding reset on every launch, so someone who
browses by Recently added chose it again each time. They are kept in the
desktop's own settings file, alongside the cache budget.
- A finished download on the desktop could only be deleted from the
Downloads page; watching it meant finding it in the library. Finished
rows gain Play, from wherever the library says it was left, and Open.
- Android showed "Queued" for a download WorkManager was holding back
on mobile data because of Wi-Fi only, or for want of any connection,
which reads as stuck. Those now say "Waiting for Wi-Fi" and "Waiting
for a connection".
Marking was per episode on the title page, or the whole title from the
library's menu. Someone joining a show at season three had to tick every
earlier episode one by one. Season headers now have Mark season watched
(or unwatched), and right-clicking an episode's tick offers Mark watched
up to here. Both go through one MarkEpisodes action with an Undo, which
SetTitleWatched now uses too.
A keyring that was reset, or a link password that changed, left a share
the desktop could only remove and add again, losing its catalog, watch
history and downloads. The phones have had a way back since B18; the
desktop's share rows now have Re-enter link, which stores the new
secret through ShareStore::replace_secrets, reopens the share and
crawls it.
- The full player overlay had no Stop: Esc leaves the page and keeps
playing, so ending an episode took a trip to the bar underneath. It has
a Stop button now, and stopping from fullscreen leaves fullscreen.
- `F` toggled the app's own idea of fullscreen, which the window manager
could change behind it (F11, a title-bar double-click). The flag now
follows the window.
- The speed menu stopped at 2x while `]` went to 3x.
Delete season, Delete show, the episode's Done badge and the Downloads
page's Delete all went straight to removing the files, with no question
and no undo - gigabytes of a season a misclick away. They now share one
confirmation that names what goes and says the share keeps the files.
The empty library said to add a link "under Shares, then refresh" with
nothing to press on the phones, and on the desktop only switched page.
All three now open the add form from there, the desktop with the caret
in the link field; and adding already crawls, so there is nothing to
refresh.
A share's name was required before the viewer had seen anything to name.
It is optional now: without one, ShareStore::add_verified takes the
shared folder's own name from the link it has just opened. The link
comes first in each form. The desktop form submits on Enter, and the
Android link field has an eye to check what was pasted.
No client crawled a share unless someone pressed Refresh, so a new
episode stayed out of the library until then, and nothing said how stale
the library was.
Catalog schema v11 adds share_crawls: when each share's last crawl
finished and was attempted, how many files it stored and how many were
new, and why the last attempt failed. A failure keeps the last good
crawl's numbers.
Every client now crawls, quietly, any share not looked at for six hours:
the desktop at launch and every half hour while open, the phones at
launch and on their existing catch-up timer. A quiet crawl says only
news ("anime: 3 new files") and leaves failures on the share's row
rather than in a toast.
Share rows on all three show "120 files - crawled 2 h ago", or the last
failure until a crawl succeeds.
Continue watching listed only part-watched episodes, so finishing one -
the most ordinary moment to want the next - dropped the show from the
shelf. Title::continue_with now reads the episode touched last: a
part-watched one is resumed, a finished one offers the episode after it
("Next: S01E04"), and one reset to the start (what leaving the shelf
does) keeps the title off. A few seconds of an episode are passed over.
The phones computed their own shelf from episode records. The bridge now
exports the engine's choice as TitleRecord.continue_index, and Play on a
title lands there on every client.
Undo on the phones re-saved a watch state with a fresh time, which both
reordered the shelf and could not express "never played". A new
restore_watch_state puts the state back as it was read.
The up-next countdown only ran over a credits chapter, so a file with no
chapters - most that are not anime - cut straight into the next episode
with no warning and no way to stay. Such a file now shows the same card
over its last ten seconds, counted on the file's own clock so it reaches
zero at the end of file, and its second button is Cancel rather than
Watch till the end.
On the phones, an episode that ends with nothing after it - the last one,
autoplay off, or cancelled - left a black screen. It now says so, with
Play next or Watch again, and Close.
Android also:
- saves the resume position from PlaybackService rather than the player
screen. With background audio on the service outlives the activity, and
a position only the screen kept stopped being saved when it went. A swipe
from Recents saves and stops instead of playing on with nothing to
return to.
- shows "Opening <episode>..." while the stream opens, where there was a
black screen for as long as the share session and block list took.
A file mpv gave up on partway ended the desktop player without a word:
the page went back to the title and the film was simply gone. It now
leaves an error toast naming where it stopped, with Retry reopening the
episode at that position.
The phone players already showed an error, but its only button was
Dismiss, so a dropped connection meant backing out and finding the
episode again. They gain Retry, which opens a fresh stream (through the
session refresh) from where the failed one stopped. A stall that lasts
twenty seconds now says so and offers the same, rather than showing
"Buffering…" for ever.
The age check and the one retry behind a refresh (B22) lived only in the
Android bridge. The desktop opened streams, offline copies and crawls
against whatever session the share was opened with, so a window left open
for a few hours failed to play or recrawl until it was restarted.
The logic moves to pstr-core as SessionAges, owned by SharedLibrary and
reached through SharedLibrary::reauthenticating. Both engines go through
it for play, warm, offline copy and (desktop) crawl.
Metadata was only fetched after adding a share (mobile) or from the
Settings buttons (desktop), so a show or episode that turned up in an
existing share stayed bare. Every crawl is now followed by a match run
when enrichment is on; `plan` makes no requests for a library that is
already matched. A stored episode guide is fetched again when a file
newer than it has no named entry, and daily for two weeks while an
aired episode is still unnamed.
Removing a share now carries its watch state to any other share holding
the same file, `pstr remove` drops the share's catalog rows, and the app
forgets catalog rows of shares no longer configured — an older `pstr
remove` left them, and every episode showed twice.
iOS, for a slow iPad:
- swipe back on a settings page uses its own edge strip; on the whole
page it also pressed the row under the finger and opened the metadata
sheet
- "Change match" from the tile menu opens once the menu has dismissed,
not after a guessed 350 ms that a slow device missed
- the activity banner tracks each running line by itself (also Android),
so overlapping work no longer leaves it spinning forever
- buttons, tiles and rows draw their press; marking watched shows at
once; remove/repair share, sign out and deletions show the banner;
the player says "Opening…" while the stream opens
- posters are read into a reserved buffer instead of a byte at a time
Sync carried only watch positions, so a second device started empty. The
per-installation history file now also carries per-key registers
(pstr-core::profile): every share with its link and custom password, the
playback, appearance and metadata settings, the TMDB key, hand-picked
matches and per-title track choices. Newer updated_at wins per key, a
removal travels as an empty register, and a first sync stamps at 0 so a
fresh install takes the library the other devices have instead of
overwriting it with defaults. Volume, mute and the device-only settings
(cache budget, Wi-Fi only, background audio, hardware decoding) stay out.
The secrets live only in the end-to-end encrypted Drive file, never in
shares.json. FORMAT_VERSION stays 1; an older build ignores the registers.
Adding a share stored it before opening the link, so a first failure left a
broken entry and every retry said "already configured". add_verified opens
the link first and stores nothing on failure, and only the new share is
crawled afterwards, so another broken share cannot fail the add.
Android drops picture-in-picture: the window sized the video wrongly and
background audio already covers leaving the app.
iOS sizes the Metal drawable on every layout, so the picture no longer
stretches after a rotation or a Split View resize. Settings show a save at
once rather than after the match run, and a failed save reverts and says
why. Both phone clients gain an activity banner naming what is running, and
the add-share, metadata and change-match forms show progress and errors in
place.
Moves to proton-sdk / proton-drive-rs 0.7.3.
The stored Proton session now holds, per account key, the passphrase derived
from the mailbox password at sign-in, and resumes through
`ProtonDriveClient::with_key_passphrases`. The mailbox password — on a
single-password account, the login password — and the key salts are no longer
stored. A session in the earlier shape, which only development builds of
1.2.0 wrote, reads as signed out: signing in again replaces it.
An account whose only second factor is a security key is refused at sign-in
with a reason, rather than asked for a code that can only fail as a wrong one.
The code prompts now say plainly that security keys are not supported.
- Desktop shows Proton's CAPTCHA instead of failing the sign-in: a wry webview
in a child process of the app (`pstr-app::verify`), confined to
https://*.proton.me, hands the token back and the sign-in is retried. Linux
packages and CI gain WebKitGTK 4.1; the Windows zip and MSI ship
WebView2Loader.dll, without which a -gnu build does not start.
- The desktop keyring splits a secret too long for one entry across several,
so a Proton session with many key salts survives Windows' 1280-unit cap.
Short secrets stay one entry, so every existing secret still reads.
- `WatchSync` runs against a small `HistoryDrive` trait and is tested end to
end against an in-memory Drive: two devices, newer wins, no re-upload of an
unchanged history, a missing file written again, an unreadable own file
never overwritten, a newer-format file left alone.
- An installation's file unwritten for 90 days is retired by the next device
to sync, after its entries are merged and carried in that device's file.
- Removing a share keeps its watch state: sync would bring it back from the
other devices anyway, and re-adding the share picks up where it left off.
- Watch history syncs once more on desktop exit (bounded to 3 s) and, on
Android, through a WorkManager job enqueued when the app leaves the screen.
- The second-factor prompts say security keys are not supported.
A Proton account is now optional alongside public links. Signing in (password,
then a second-factor code and a mailbox password where the account has them;
a CAPTCHA gate is solved in the Android app) stores one session blob in the OS
credential store under `proton-account`, writing back every refreshed token
since refresh tokens are single-use. With an account, folders of the viewer's
own Drive — My Files, other devices, shared with me — can be added as shares
through the same `ShareClient` surface as a public link.
Watch history syncs through a `proton-stream` device in that Drive: one
`watch-history/<installation>.json` per installation, so no two devices ever
write the same file. Each sync reads the others and merges per episode, newer
`updated_at` wins, with a deterministic tie-break. It runs at launch, after
sign-in, whenever the player closes and every five minutes. A session that can
no longer refresh signs the app out instead of failing on every launch.
The CLI gains `login`, `logout`, `account`, `browse` and `sync`.
Also in this change:
- Android offline downloads no longer look stuck (B64): two transfer at once,
through a copy stream with no read-ahead, with a smoothed transfer rate.
- Choosing a metadata match by hand now enriches the title too, instead of
keeping the old match's episodes and artwork.
The catalog schema is unchanged.
An MSI upgrade left the app without libmpv-2.dll until it was installed a
second time (B62). Costing runs before RemoveExistingProducts, saw the old
DLL at the same version and skipped the copy, and removing the old product
then deleted it. Both installer definitions now set REINSTALLMODE=amus;
every file is private to the install directory.
A taskbar pin lost its icon after a while (B63). The exe carried no icon
resource, and the Start Menu shortcut's icon lived in the per-ProductCode
installer cache that each upgrade deletes. build.rs now compiles the .ico
into proton-stream.exe, and the shortcut has no Icon of its own (and in the
WiX build is no longer advertised), so both show the exe's icon.
Bring the phase 2 library to Android. Search, sorting and franchise folding
move from the desktop app into pstr_core::browse, so both clients find,
fold and order the same titles the same way.
The bridge gains AndroidEngine::arrangement, which returns the grid's tiles
and up to three shelves as keys into the library's titles. Each TitleRecord
now carries its display name, wide art, format, season, studios, directors,
tags, airing state and franchise. The library bar has a sort menu with the
franchise toggle, folded tiles show a +N badge, and shelves sit above the
grid. The title page shows the same facts, tags and franchise row as the
desktop. Settings -> Appearance gains Title names.
The default title name is now English, which is what Android always showed.
A folded franchise now sorts by its lead's displayed name rather than by its
alphabetically first member.
The AniList query now keeps the start date, season, banner, popularity,
status, next airing episode, tags, main studios, director and story
relations, as TitleDetails in one JSON column (schema v10). Matches
stored without them are fetched again by id, never searched, so
hand-picked matches stay; refreshing the same entry keeps its episodes
and fanart. Enrichment records the entries it walked. node_first_seen
remembers when each file appeared, across recrawls.
pstr_core::franchise groups titles that share a provider id. The grid
folds each franchise into its lead with a +N badge, and the title page
lists the whole franchise in release order. pstr_core::shelves picks
rows by airing status, director, studio and genre. A row needs three
franchises, so one franchise's titles never make a row on their own.
Desktop:
- Covers are the default grid (Settings > Appearance > Library tiles).
- The hero and title band use fanart or banner decoded at 1920 px.
- New sorts: recently added, newest, highest rated, most popular.
- Titles can show the share's, English or romaji name; search matches
all of them.
Android carries TitleDetails through a hand-picked match; its screens
are unchanged.
AniList's per-season lookup searched for "<title> 2nd Season" and the
scorer picked the base entry again, so Attack on Titan seasons 2-4 and
Jigokuraku season 2 were captioned with season one's names. Episode
names came from streamingEpisodes, which is partial and numbered however
a streaming site counted. Bleach S17 and Mushishi S02/S03 had none.
AniList now walks SEQUEL relations from the match and takes each
entry's episodes from ani.zip, which carries TVDB's season, number and
absolute number. An episode row keeps all three plus its place in the
AniList entry. EpisodeGuide::get matches whichever numbering a file
uses and never answers from another season.
Episodes and fanart are stored as one enrichment. The episode list is
replaced even when empty, and the enrichment is recorded as asked, so a
film is not re-asked every run. Storing a different match drops the old
entry's episode rows. AniList's 4.75:1 bannerImage is no longer stored
as the 16:9 backdrop; ani.zip's 1920x1080 fanart is used instead.
Schema v9 adds the columns and deletes the AniList episode rows and
banners. Matches are kept. Desktop, Android and the CLI now share
service::plan and MetadataService::work. The settings text names
ani.zip. Audit and roadmap in docs/METADATA.md; bug entry B61.
The icon follows proton-drive-linux's logo: a violet gradient tile with
a darker lower edge, and a white play mark with a soft shadow, cut into
the blocks a revision is fetched in. The SVG, the window PNG and the
Windows .ico are rendered from one source.
On Android it is an adaptive icon with a monochrome layer for themed
icons, and the notification glyph is the same mark. The app is named
"Proton Stream"; the launcher shows "Stream", since "proton-stream" was
cut to "proton-st…".
Toasts through WinRT, under the app id io.narl.proton-stream. The id is
registered for the current user under HKCU\Software\Classes\AppUserModelId
with a display name and icon on the first notification of a session, which
is what lets an app with no package or installer-made shortcut show one. If
that write fails, the toast goes out under PowerShell's id rather than not
at all.
A second launch hands its request to the open window and exits: over a
Unix socket in the user's runtime directory, or a named pipe on Windows
whose name carries a random key from the user's config directory, so
another account cannot take the name first. The open window comes forward,
and a share link given as the first such argument opens the Add form with
it filled in. Setup failures are logged and the app runs without the lock.
A button in the player's title strip, or E, opens the title's episodes
down the right of the picture, a season at a time, starting on the
playing season and scrolled to the playing episode. Each row has its still
(which plays it), its name, and whether it is playing, watched or part
way. The controls stay up while the list is out, and Esc closes it before
it leaves fullscreen or the player.
Every played episode, newest first, under Today, Yesterday, a weekday or a
date. A row shows the still (which plays it), the title (which opens it),
the episode, and where it stopped or that it was finished. Removing a row
resets that file to never played, with an Undo. History sits second in the
top bar, so Ctrl+1 to Ctrl+5 now run Library, History, Shares, Downloads,
Settings.
A provider with an API key read it from the OS credential store inside
Engine::new, which is a D-Bus round trip to a keyring that may have to
unlock first. It is now built on the blocking pool. Until it lands, poster
fetches, match runs and match searches wait for it rather than concluding
there is no provider and marking posters missing. A settings change made
in the meantime wins over the launch-time build. The share list is read
on the blocking pool too.
theme::space, theme::radius and theme::motion name the steps the UI is
drawn with. Literal gaps, corner radii and animation durations across ui/
now come from them, which folds a few near-misses onto the scale (10 and 14
point gaps onto 12, 10 point radii onto 12, 0.10 s hovers onto 0.12 s).
Page fades and toasts share one ease_out curve.
The watched and download buttons were laid out with with_layout(right_to_left,
Center), which takes every point of height the scroll area has left and
centres the buttons in it. Give the cluster the still's height instead.
Headings are set in Inter SemiBold, everything else in Inter Regular, so a
title sets the same on every desktop. Media and navigation glyphs (⏭, 🔊, ⛶,
★) come from the Phosphor icon font instead of whichever fallback font had
them. The CJK fallback still loads on its own thread and now joins the
bundled fonts instead of replacing them. The window carries the app icon
on X11 and Windows.
- a page fades in over 140 ms when the page changes, rather than cutting
- Ctrl+S in the player saves the frame, without subtitles, to
Pictures/proton-stream as "Title S01E04 12-34.png"
Tiles and episode stills light up under focus as they do under the
pointer and scroll into view when the arrow keys or Tab reach them;
Enter opens or plays. Global keys now check for a focused text field
rather than any focused widget, so / and Esc still work over a tile.
A toast can carry a button. Marking a whole title watched or unwatched
from its tile menu, and taking it off Continue watching, both lose
state the viewer may have wanted; each now says what it did with an
Undo that puts every changed watch state back as it was.
- the streaming cache's budget (1-16 GiB) is chosen in Settings, shows
what it holds, and can be emptied; a lower budget evicts at once
- hardware decoding can be turned off for a driver that shows green
frames, from the next file
- both live in desktop.json rather than the playback prefs shared with
Android, whose bridge record would reset them on every save. A file
that will not parse is never overwritten
The film keeps playing small at the left of the transport bar while
browsing, and is the button back to the player page. It sits beside
both rows of controls, so the bar is no taller for it, and renders at
thumbnail size four times a second rather than at the film's rate.
The title watched last, or with nothing part-watched one of the titles a
provider has a backdrop for, turning over once a day. Large art behind
the name, rating, genres, three lines of overview, and Resume / More
info.
- a finished batch of downloads, and a crawl long enough to walk away
from, notify through org.freedesktop.Notifications when the window is
not focused; once per batch, not once per file
- Ctrl+V with a share link outside a text field opens the add form
with the link in it