- player YB2: keepalive pagehide beacon (api.saveProgressBeacon) so the resume
position isn't lost on F5/close within 5s of the last checkpoint (mirrors Plex).
- player YB4: bound consecutive unplayable items in auto-advance so an
auto-advancing/loop=all queue can't spin over a run of dead videos.
- downloads B6 (client): localise structured quota/edit errors centrally in
api.req() via localizeDetail() + Intl.NumberFormat (fixes HU/DE + decimal sep);
+ 3-locale download error strings.
- channels FB1: focusChannelToken bump so re-clicking the same channel re-seeds
the search box; FB2: syncSubs invalidates feed/feed-count (set can change now).
- config CB2: reseed the ConfigPanel draft on a key-set dataVersion + explicit
post-save token, so a mid-edit refetch can't clobber an in-progress edit.
- config AB3 (UI): a blank allow_empty field stores "" instead of resetting.
- admin SB2/SC2: confirm + success toast on demo-whitelist remove and deny-invite
(+ trilingual strings); SC1: disable only the acted-on row (pendingId), not all;
SB3: pause the 1s scheduler countdown ticker while the tab is hidden.
- messages CT4: only invalidate conversations/unread when the incoming-message
count actually changed, not on every 20s poll; MB3 (client): react to the live
unread ping (onUnread → invalidate); MC3: extract e2ee installKey (setup/unlock);
CC2: hoist POLL_MS to messaging.ts; MB4: document the reload-scoped socket.
- Delete dead top-level downloads.rename.* i18n block (en/hu/de); the
metadata modal moved to downloads.edit.*. Keep downloads.actions.rename
(still used by ProfileEditor).
- Rename misnamed state renameJob/setRenameJob -> metaJob/setMetaJob in
DownloadCenter (it opens MetaEditModal, not a rename dialog).
- Extract the duplicated [downloads]/[download-index]/[download-usage]
invalidation trio into lib/downloads.ts invalidateDownloads(qc);
used by DownloadCenter and DownloadDialog.
Behavior-neutral. tsc green, knip shows no new unused export, JSON valid.
The edit (pencil) action now edits a download's full display metadata — title,
channel name, channel link and any number of extra reference URLs — instead of
just the name. The channel and links render as clickable links on the library
card, and the channel link is auto-filled from the source (yt-dlp channel_url)
when available. Shared watch pages resolve the same per-download overrides, so a
rename/channel/link edit is reflected on the public /watch page too, with every
link clickable.
Adds migration 0049 (media_assets.uploader_url; download_jobs.display_uploader,
display_uploader_url, extra_links) and generalizes the rename endpoint into a
metadata update with URL validation. EN/HU/DE strings included.
Every download now records the clean source page URL and shows it on the
Downloads page (open in a new tab, or copy to clipboard). The worker stores
yt-dlp's canonical webpage_url on the asset (migration 0043 adds
media_assets.source_webpage_url); the serializer prefers it and falls back to a
URL derived from source_kind+source_ref, so YouTube, external YouTube links and
external URLs (e.g. Facebook reels) all get a correct reference, and queued/older
rows work before the worker fills it. Edit clips return null (a clip's source is
the user's own earlier download, not a web page). i18n en/hu/de.
A shared-with-me item only had a download button. Add two actions (Share is intentionally NOT
offered — no chain re-sharing of someone else's file):
- Edit: the editor now accepts an accessible (owned OR shared) source, so editing a shared video
produces the editor's OWN clip in their library (counts against their quota, fully theirs
including share); the source file is only read. Route uses _accessible_job.
- Remove from my list: DELETE /api/downloads/shared/{job_id} deletes only the recipient's share
grant — the owner's job and physical file are untouched (per-user dismissal).
i18n en/hu/de. Verified in a real browser (edit a shared 70-min video → own 2:31 clip; remove
confirm shows 'won't delete the owner's file').
The admin System tab only listed users with an existing footprint, so a quota could be set only
after someone downloaded something. Add a 'Set a user's quota…' picker (over /api/admin/users,
excl. demo) beside the Per-user footprint heading that opens the quota editor for ANY user — the
quota GET/PUT endpoints already resolve defaults + create a row on demand, so no backend change.
Also show an empty-state line when no one has a footprint yet. i18n en/hu/de.
Rework the share dialog into two clear modes and add the public /watch player page:
- ShareDialog: (A) 'Share with a user' — autocomplete picker over registered users (was a blind
email box that 404'd on non-users); (B) 'Share a link' — create/list/copy/revoke public links
with allow-download toggle, optional expiry (1/7/30d), optional password; per-link view count.
- WatchPage: standalone login-free player at /watch/<token> (routed in main.tsx like /privacy),
self-contained mini-i18n (en/hu/de by browser language); password gate → unlock → play; shows a
Download button only when the link allows it.
- api: ShareLink/ShareRecipient types + link CRUD + recipients; share i18n (en/hu/de).
Verified end-to-end in a real browser: user picker, link create, public playback, stream-only vs
downloadable, password gate + unlock, no console errors.
VideoEditor modal on a finished Library download: HTML5 <video> scrubber, filmstrip timeline
(lazy server storyboard sprite) with draggable in/out handles, draggable/resizable crop overlay,
per-edit Precise (re-encode) vs Fast (stream-copy) cut toggle, split-into-N fan-out (N trim jobs),
optional clip name. Edited clips show a 'Clip' badge; 'editing' phase gets a % bar. New api
enqueueEdit/downloadStoryboard/storyboardImageUrl + EditSpec type; editor i18n (en/hu/de).
ffmpeg post-steps have no byte-progress, so instead of a silent 'Processing' the row now names
the current step (Merging / Extracting audio / Embedding thumbnail / Removing sponsors / Writing
metadata) with an indeterminate pulse. Byte-progress phases (video/audio) keep the % bar.
i18n en/hu/de.
The progress bar looked broken (95% -> 5% -> recount) because yt-dlp downloads the video and
audio streams separately (each 0->100%) then merges — with no indication of which step is
running. Now:
- worker labels the phase from the stream codecs (video / audio) and via a postprocessor hook
(merging / processing), so the user sees what's happening
- DownloadCenter shows the phase as the status and renders an indeterminate pulse (no bogus %)
during merge/processing; phase i18n en/hu/de
- yt-dlp retries (3) + fragment_retries (5) so transient network blips self-heal instead of
failing the job (seen once as a DNS 'No address associated with hostname' error)
Verified: phase transitions queued -> video -> processing -> done.