fix: review round 1 — row press after a rename, related reset, dead items on removal, honest Prev/Next

- A press that begins while a field in the row is edited only ends the editing (rowOpen latches it
  at pointerdown); the card layout's row press is now tested.
- A related video reset in the player forgets the session's stored position.
- Removing the playing video passes over known-dead items and steps the way it went.
- Prev/Next off only because of dead items stay focusable and say why (HU/EN).
- The docked bar's scroller leaves room for focus rings.
- A focus-opened About card is not closed by its own scroll into view.
- EN "Clear watch progress" for the video reset, distinct from a page's "Reset view".
- The signed-out title read goes to the page yt-dlp read; the retitle writes are conditional.
- Tests: the cap of three errors, the bar's own Fullscreen, the group glyph, the backfill's reads.
This commit is contained in:
2026-11-01 05:36:59 +01:00
parent 245c0a2019
commit c910cf2f20
25 changed files with 411 additions and 63 deletions
+10 -6
View File
@@ -7,7 +7,8 @@ and safe to re-run: it only touches assets still missing the field, and only re-
Facebook sources (a direct media URL like a reddit HLS manifest carries no channel and its signed
URL may have expired — those just get a locally-generated poster instead). A Facebook download
titled with the source's placeholder ("Facebook", "Video") is retitled from a signed-out read of its
page, and so is each download name that only repeated the placeholder.
page, and so is each download name that only repeated the placeholder. That page read
(`public_title_info`) also titles every new download, from the worker.
Run once per environment:
docker exec siftlode-worker-1 python -c "from app.downloads import backfill; print(backfill.run())"
@@ -16,7 +17,7 @@ Run once per environment:
from urllib.parse import urlsplit
import yt_dlp
from sqlalchemy import select
from sqlalchemy import select, update
from sqlalchemy.orm import Session
from app import logs
@@ -88,10 +89,13 @@ def _retitle(db: Session, a: MediaAsset, adapter: adapters.SourceAdapter, url: s
named = normalize_title(adapter.title(info))
if not named or adapter.is_placeholder_title(named):
return False
jobs = db.execute(select(DownloadJob).where(DownloadJob.asset_id == a.id)).scalars()
for job in jobs:
if job.display_name == a.title:
job.display_name = named[:DISPLAY_TEXT_MAX]
# One conditional write, not a read and a compare: a name its owner gives while this runs (the
# poster step can take seconds before the commit) is not overwritten by what was read before it.
db.execute(
update(DownloadJob)
.where(DownloadJob.asset_id == a.id, DownloadJob.display_name == a.title)
.values(display_name=named[:DISPLAY_TEXT_MAX])
)
a.title = named
return True
+14 -5
View File
@@ -7,6 +7,7 @@ from datetime import date, datetime, timezone
from pathlib import Path
import yt_dlp
from sqlalchemy import update
from yt_dlp.utils import download_range_func
from app import logs, sysconfig
@@ -587,8 +588,10 @@ def _download(
# too. Same probe the upload ingest uses.
vcodec, acodec, width, height, duration_s = probe.resolve_media_facts(produced, info)
# A placeholder title with nothing behind it ("Facebook" from a signed-in read) borrows the
# page's text from one signed-out read; the download itself stays the signed-in one.
titled = backfill.public_title_info(info, url, adapter, asset_id)
# page's text from one signed-out read; the download itself stays the signed-in one. The page
# yt-dlp read, not the link queued: a group post's link is rewritten to its video, and
# signed out the post itself is the login wall.
titled = backfill.public_title_info(info, info.get("webpage_url") or url, adapter, asset_id)
meta = storage.MediaMeta(
video_id=info.get("id") or source_ref,
title=normalize_title(adapter.title(titled)) or source_ref,
@@ -1120,14 +1123,20 @@ def _process_clip(job_id: int, asset_id: int) -> None:
fields: dict = {"status": JobStatus.DONE, "progress": 100, "phase": None}
# A video outside the catalog was queued as "Clip (clip)": the fetch has its title now.
placeholder = service.clip_title(None, None)
named = None
if asset and fetched_title and asset.title == placeholder:
named = service.clip_title(fetched_title, None)
asset.title = named
if job_row := db.get(DownloadJob, job_id):
if job_row.display_name == placeholder:
fields["display_name"] = named[:DISPLAY_TEXT_MAX]
title = asset.title if asset else None
finalized = service.finalize_if_running(db, job_id, **fields)
if finalized and named:
# Only a name that still repeats the placeholder, in one conditional write: one its
# owner gave meanwhile stays.
db.execute(
update(DownloadJob)
.where(DownloadJob.id == job_id, DownloadJob.display_name == placeholder)
.values(display_name=named[:DISPLAY_TEXT_MAX])
)
db.commit()
if finalized:
_notify_download_done(db, user_id, title, job_id, kind="edit")