Commit Graph
2749 Commits
Author SHA1 Message Date
peter 3a09f0d78d release: 0.120.1 2026-11-14 21:34:49 +01:00
peter 06c0ab5fb6 fix(files): a row's picture fits its box on iPad
In a table cell WebKit resolved the picture's h-full against the aspect-video box as the poster's own height, so the box showed a native-size sliver of it. The box now has a fixed 16:9 height, as the other pictures in tables do; an e2e check runs in the WebKit and iPad projects.
2026-11-14 21:34:33 +01:00
peter 903e56fba5 release: 0.120.0 (bug/sliding-session) v0.120.0 2026-11-14 21:12:12 +01:00
peter 5a9ac019ae release: 0.120.0 2026-11-14 20:53:01 +01:00
peter 7a1487f554 fix(auth): a cancel at Google lands where the browser is; the probe says why it refused
A refused session keeps its cookie, so the cancel branch, keyed on the cookie's account, sent a
signed-out browser to the in-app toast the login page never shows: it now asks whether the
session holds. The probe's log line named every refusal 'its account no longer holds'; it now
carries the reason. The pruning docstring says again that a renewal may land a request's time
after the revoke, and the test pins the day of margin. The test clock clamps in the zone asked for.
2026-11-14 20:07:13 +01:00
peter 7dd50db9b7 fix(plex): the rail says once that its facets failed, with Retry
A failed facets load put the same muted line in up to five sections with nothing to press (the
query never retries by itself), and only the Year section announced it, so a reader who hid that
section heard nothing. The rail now shows one StateMessage with Retry, in the Year section or in
its foot when Year is hidden; the facet sections wait for their data as before.
2026-11-14 20:07:13 +01:00
peter 8d6aa05bc8 fix(auth): a cookie from before session ids signs in once more; a session's age is its renewal
A sign-out sent with a cookie that had no session id revoked nothing, so a request of its still
in flight signed the browser back in; its id, drawn from the raw cookie, also changed whenever the
cookie was re-signed. Such a cookie is now refused, like one with no epoch: every one would have
run out within 14 days anyway. And any route that writes the session re-signs the cookie (the
anonymous Google sign-in start too), so a refused one could be kept fresh past the row revoking
it: a session is now as old as its renewed stamp, which only a sign-in and a renewal write.
2026-11-14 19:46:35 +01:00
peter 4dc9846eba test: the cookie signer and test modules read the never-back clock too
The docker VM's clock steps back: itsdangerous refused a cookie signed in the next second (a
random 401 right after a sign-in), and a test compared an app stamp with the raw wall clock.
2026-11-14 19:32:02 +01:00
peter f1d0845923 fix(auth): refusing a revoked session no longer deletes a newer sign-in's cookie
The refusal cleared the session, so a request in flight with the previous account's cookie
answered with a cookie deletion that could land after a sign-in as someone else. A revoked id is
refused on every request anyway. Also a test for the pruning of old revocations.
2026-11-14 19:32:01 +01:00
peter be6bd586d7 fix(filters): a pick that hides More… keeps the focus, counts read alike, Plex facets say they failed
Picking in the year dialog the one year the chips hid took More… away with the dialog, and the
focus fell to <body>: it now goes to the picked year's chip. A count badge is compact in the UI
language everywhere (a chip read "17200" beside its cell's "17.2K"). While the Plex facets load
or fail, the sections they fill say so instead of vanishing as if the library had none.
2026-11-14 19:32:01 +01:00
peter 36f9aefb76 fix(downloads): a file asked for again during an over-cap pass stays whatever the clock says
The eviction pass called a file freshly used only when its last-access stamp was later than the
pass's own clock. A hit stamped just before the pass began but committed after its SELECT, or
stamped by a clock that had stepped back (the test VM's does), read as old, and the file the user
had just asked for was evicted. The pass now keeps the stamp its SELECT saw and skips a file
whose stamp changed since.
2026-11-14 19:05:43 +01:00
peter 0eca18bcb4 fix(filters): the year dialog fits a tablet, walks by arrow keys, names one year once
- a decade cell is a 36-high box with its count under the year (owner A)
- arrow keys walk the cells: along a decade, then to the nearest year below
- clearing the kept older year hands the focus on instead of dropping it
- More... shows while a year with titles is hidden, as a plain chip
- the decade caption is not read twice; the Plex range fields have names
- one Plex year is labelled 1999, not 1999-1999, via oneYear/singleYear
- tests for the Plex wiring and the feed year clearing the upload date
2026-11-14 17:56:37 +01:00
peter 02714e4986 fix(auth): a cookie that lands after sign-out no longer signs the browser back in
A renewal answers with the session its request started with. Each session now
carries an id; signing out, or signing in as someone else, revokes it
(revoked_sessions, migration 0120), and a cookie carrying a revoked id is
refused. A cookie from before session ids gets one drawn from the cookie
itself. The race after log-out-everywhere fails closed and stays.
2026-11-14 17:56:36 +01:00
peter d265c84c8b fix(filters): the year dialog's sort caption keeps its distance from the toggle 2026-11-14 15:23:16 +01:00
peter 1dff809268 feat(filters): pick one year from a decade grid, in the feed and in Plex
A shared YearPicker: the recent years as chips with their counts, and a
More dialog that lays every year out by decade (sortable by count), so a
ninety-year library fits on one screen. The feed's scrolling popover gives
way to it; the Plex rail gets it above its range fields, a pick setting
both ends. The Plex facets now count titles per year, the year filter left
out.
2026-11-14 15:08:50 +01:00
peter 94f9ffdb1c fix(auth): a session in use slides instead of expiring on day 14
Starlette re-signs the session cookie only when the session changes, so a
browser in daily use was signed out 14 days after sign-in, mid-episode.
The first authenticated request a day after the last renewal now writes a
fresh stamp, which re-signs the cookie; only 14 idle days end a session.
The epoch stays the revoke.
2026-11-14 14:43:04 +01:00
peter 3bb71a4bc3 release: 0.119.2 (bug/fb-date-translit) v0.119.2 2026-11-14 01:34:13 +01:00
peter 87ab33b03c release: 0.119.2 2026-11-14 01:21:11 +01:00
peter 452ff042ff fix(downloads): Facebook files get their upload date, non-Latin names their letters
Since 2026-10-28 the signed-in Facebook read gives no upload date, so
every Facebook file since was named 0000-00-00 in Season_0 (31 on prod,
none before). The signed-out read that already titles these downloads
carries the date, so it now lends that too when the signed-in read has
none (backfill.public_title_info is now public_page_info).

On-disk names dropped every letter outside the Latin script, so a Hebrew,
Cyrillic, Greek or CJK uploader went to one shared "untitled" folder
(prod asset 503). anyascii now writes them in Latin letters
(Сергей Иванов -> Sergei_Ivanov); Latin and accented names are unchanged.
2026-11-14 01:15:17 +01:00
peter 25198a936f release: 0.119.1 (bug/fb-share-group-post) v0.119.1 2026-11-14 00:49:24 +01:00
peter 9d979a5202 release: 0.119.1 2026-11-14 00:36:34 +01:00
peter fb3aca4c9c fix(downloads): a shared or feed-opened Facebook group post downloads its own video
A Facebook share link (/share/v/…) redirects to a group post, but only the
permalink form went through the post-page read, so yt-dlp followed the
share link itself and downloaded an ad from the signed-in group feed
(prod job 471). The link of a post opened from the group's feed
(/groups/<g>/?multi_permalinks=<id>) was not recognised at all, and yt-dlp
answers it with "Unsupported URL" (prod job 472).

With a cookie, a share link is now followed in the same request that reads
the post page, and a feed link is read as the post it names. Without one,
the feed link goes to yt-dlp in the permalink form it knows. A post page's
"no single video" verdict now names the post for a share link too.
2026-11-14 00:29:59 +01:00
peter 4f78877201 test: the app's clock never goes back in the test suite
The docker VM the suite runs in steps its wall clock back now and then (measured: 2 steps in 20 s, up to 0.5 s), so rows a test made one after another could read out of order (a files list sorted newest first, a scheduler's last runs). Every app module that took datetime from the datetime module reads a clock that never repeats or goes back; the scheduler test's own fixture for this goes.
2026-11-13 03:02:00 +01:00
peter 77f8170153 release: 0.119.0 (feature/e61-gif) v0.119.0 2026-11-13 02:32:25 +01:00
peter 0fd0f1f637 release: 0.119.0 2026-11-13 02:18:55 +01:00
peter 7371cdeb94 refactor(downloads): the GIF span helper stays inside lib/gif 2026-11-13 01:42:36 +01:00
peter abb18d4d8c fix(downloads): GIF review round 3 — every length readout, live regions in place
A refused GIF's length reads rounded up everywhere the editor shows it (the segment and the kept summary of one), and the GIF notes and estimates sit in live regions that are there before GIF is picked, so a note already true then is said. Tests hold the clip panel's readout and which segment a note names; DESIGN.md claims only what the contract test holds.
2026-11-13 01:16:21 +01:00
peter 5bf13c8701 test(scheduler): runs started in a row start in that order
The scheduler tests open several runs within microseconds and read them back newest first, by start time. A container's clock on a VM can step back between two readings, which sorted the third of three runs before the second; the tests now run on a clock that never repeats or goes back.
2026-11-13 00:31:57 +01:00
peter fe6af1445f fix(downloads): GIF review round 2 — cap as sent, shown length, retry, Join reason
The panels judge a GIF's length in whole milliseconds between the ends as sent, the server's own rule, and a refused length reads rounded up. A shared picture says it failed only once its retry failed too. Join's reason shows on a key's focus; the editor's failure toasts name GIFs. The edit route's cap is test-held, and DESIGN.md records the GIF UI.
2026-11-13 00:19:53 +01:00
peter 8845d1aa49 fix(downloads): GIF review round 1 — shape, HDR, caps, audio, copy
- A GIF squares its pixels before the size, so an anamorphic source keeps
  its shown shape (a 720x480 DVD at 16:9 was 480x320).
- An editor GIF of an HDR upload is tonemapped, as a clip GIF is.
- The length caps are judged in whole milliseconds: a range exactly at a
  cap is no longer refused for a float hair over it.
- A GIF is made of a video only: an audio file is refused with its reason
  and the editor offers no GIF for it.
- A GIF clip keeps no audio track: the track neither splits its cache key
  nor fails it.
- The editor's GIF notes keep their live region mounted; one size reads
  as one ("about 1 MB"), none is shown with nothing kept; the size choices
  show their unit; a shared picture that fails to load says so.
- Copy: the two-sentence GIF notes keep their last stop (a copy-rule test
  now holds it); the English warns of "only a still picture".
2026-11-12 23:24:25 +01:00
peter 0b92b40db5 fix(downloads): a GIF's size warning judges the size it shows 2026-11-11 02:19:56 +01:00
peter 27ccf253f5 feat(downloads): make a GIF in the clip panel and the editor, and show a shared one as itself
The clip panel and the editor get a Video / GIF choice with the GIF's size, the size it is expected
to come out at, and what that size means (no moving link preview over 15 MB, refused over 30 MB).
A range longer than a GIF may be leaves Make unavailable with its reason. The editor makes one GIF
per kept segment; Join says why it is unavailable instead of going dead. The share page shows a
picture as itself with its Download, and a picture in a folder opens above the list.
2026-11-11 02:10:07 +01:00
peter c221452a00 feat(downloads): a clip or an edit can be a GIF, shared as a picture
A clip and an editor range can come out as a silent GIF (long side
320/480/640, 12 fps, at most 30 s, one palette per GIF); one over 30 MB
is not kept. A shared picture is served inline under the preview's
sandbox, and a GIF's link card is the GIF itself up to 15 MB, the still
for Facebook's crawler.
2026-11-11 01:50:55 +01:00
peter fe38c07e7e test: the sub-view census reads each source once
Read three times, the scan ran past the 5 s test timeout when the gate
loaded the machine.
2026-11-11 01:50:54 +01:00
peter 45b5e19fb8 release: 0.118.0 (improvement/e59-small-residuals) v0.118.0 2026-11-08 01:23:33 +01:00
peter e08278fae2 chore(release): 0.118.0 2026-11-08 01:05:31 +01:00
peter 350913b6c2 test: a watch's weight by age, and each contrast test under its reason
`draw.recency` weighs every watch of both affinities: an undated watch
and a future one now have a test (deleting either branch fails it). The
over-the-picture contrast test moves below the form-on-chrome test, so
each sits under the comment that explains it.
2026-11-08 00:49:22 +01:00
peter 470caacfb0 fix(plex): the slow line says both rates in one unit
Each rate took its own unit, so a link under a megabit read "needs 1.4
Mbps, 800 kbps arrives". `fmtBitrates` picks the unit the largest takes
for all of them. The away-window header now says what ends an absence
outside hls.js (the answer) and after the error (the viewer's retry).
2026-11-08 00:49:22 +01:00
peter e8b9bdd7c1 fix(playlists): "+N without length" counts every video the sum left out
The label said "without length" but the count held only live videos, so
a video not fetched yet, or gone before it was, was left out of the sum
and named nowhere. The server's `Video.without_length` and the client's
`isWithoutLength` are now one rule (no length, whatever the live
status), and the field, the key and the helpers are named for what they
count. The comments that still quoted "+N live" say the current copy.
2026-11-08 00:49:21 +01:00
peter 55cbbfb6e4 fix(channels): the Length column shows the sum alone
The "+N without length" line did not fit the 4.5 rem column (88 px of
text in a 56 px cell) and spilled into Tags. The owner wants no such
line there: the cell shows the summed hours, and the channel list no
longer sends a count nothing reads. A playlist's total keeps it.
2026-11-08 00:10:28 +01:00
peter 4d058cba81 fix(plex): an error over the picture is readable on any frame
The error line kept the theme's red, which a light theme makes dark: 1.6:1 on the 75 % panel over a
white frame. The panel is now black 85 % under media-scope, so the error is the media red in both
themes (5.47:1 at worst, white text too); a contract test measures it on the composited panel.
2026-11-07 22:04:22 +01:00
peter 872b583f06 fix(card): a total's left-out videos are "without length", not "live"
The count holds archives and upcoming streams too, which a card names a stream or upcoming.
2026-11-07 21:58:36 +01:00
peter 90931b4750 fix(plex): a playlist that never answers is asked for again; the slow line reads this stream
A playlist timeout was resumed in the same session, but hls.js reloads no manifest on startLoad: no
request, no error, a spinner for good. It is now asked for again like a server away. An absence
also ended only on a fragment, which native HLS never reports, and a viewer trying again after the
error started with the spent window; both now start a fresh one. The slow-connection meter was
reset only when hls.js was built, so a direct answer showed the previous stream's numbers, and a
replaced session's late fragment still counted; its rates now use the stats overlay's units.
2026-11-07 21:58:36 +01:00
peter a6f1a26658 docs(plex): the transcode comments say the CPU-decode route 2026-11-07 21:48:39 +01:00
peter 4ed07388e8 test(feed): an off-catalog video's length, and async readers of the raw column
The YouTube lookup's length had no test, and the guard on who reads the raw duration skipped async
functions and module-level code.
2026-11-07 21:48:38 +01:00
peter bb91d0a22d refactor(discover): both affinities weigh a watch by one rule
The YouTube channel affinity and the Plex one each had the recency decay, the undated-watch rule and
the scale to the strongest; app.draw now holds them. Tests hold that another user's history leans
neither draw, the rail's blind slot, and that a show leans on show recommendations.
2026-11-07 21:48:38 +01:00
peter e7e8d8236e fix(downloads): the egress filter keeps what yt-dlp fetches itself, and shows no phase
f4m, ism, a bare m3u8 (native under hls_prefer_native) and the DASH generator fetch through the
proxy, but were dropped as if ffmpeg dialled them. The filter also took the job's postprocessor
hook, twice, and painted every guarded job "processing" before a byte arrived. Tests now hold an
allowed tunnel, the worker's own gate, the single-URL branch, and that the pipeline and the
backfill turn the gate on while YouTube does not.
2026-11-07 21:48:38 +01:00
peter fc65b8ab83 fix(playlists): a refused push spends none of the push allowance
The cooldown was checked before the list was found pushable and before its sync claim, so Watch later
or a list another sync held used up the push allowance. It now counts only a push that runs. The
docstring that called a YouTube mirror unpushable is corrected: its local edits sync back.
2026-11-07 21:48:38 +01:00
peter 26ed8855c9 fix(feed): an out-of-range seed or cursor is refused, not a 500
A seed past a float overflowed the shuffle's start point, and a cursor holding Infinity or a 400-digit
key overflowed its int/float read: both answered 500. The routes bound the seed (422), the start
point folds it to 32 bits, and the cursor read counts an overflow as a bad cursor (400). A plan test
holds that the walk reads ix_videos_shuffle_key with no sort over a realistic scope.
2026-11-07 21:48:15 +01:00
peter 9778bdf351 fix(plex): a codec the GPU cannot decode is decoded on the CPU and still encoded on the GPU
The prod iGPU has no VC-1 Advanced decode ("No support for codec vc1 profile 3"), so every VC-1
session died on the GPU and fell back to libx264, which needs ~17 s for the three warm-up segments:
a start or a seek took ~19.5 s. VC-1, XviD and DivX now take CPU decode -> hwupload -> h264_vaapi
(three segments in 5.5 s for a 1080p VC-1 remux, measured on prod hardware with this argv).

The transcode_hw facet now says whether the GPU encodes: VC-1/XviD/DivX count, and so does an HDR
film (the OpenCL tonemap runs on the GPU; the facet said software). Migration 0119 rewrites the
stored facet and re-queues 10-bit items for the HDR probe.
2026-11-07 18:39:15 +01:00