From 81eacabf5ff1751ded0c259e11822403810d368b Mon Sep 17 00:00:00 2001 From: Jusii Date: Wed, 13 May 2026 16:46:47 +0300 Subject: [PATCH] Refresh stale source_dir on already-queued rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user locks a clip on the dashcam the file moves from /DCIM/Movie to /DCIM/Movie/RO. reconcile() previously short- circuited on "filename already in queue" and never picked up the new path, so the sync worker kept hitting the old URL and burned its retry budget on HTTP 404 until the row was marked failed. The fresh listing already has the correct path; just propagate it to the existing row when state is pending or failed. done / downloading / gone rows are intentionally left alone — the source_dir is historical at that point. --- tests/test_refresh_listing.py | 37 +++++++++++++++++++++++++++++++++++ web/services/queue.py | 20 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/tests/test_refresh_listing.py b/tests/test_refresh_listing.py index 2642732..87efde2 100644 --- a/tests/test_refresh_listing.py +++ b/tests/test_refresh_listing.py @@ -108,6 +108,43 @@ async def test_refresh_picks_up_clips_added_between_calls( ] +async def test_refresh_updates_source_dir_when_clip_moves_to_ro( + db: Database, +) -> None: + """If the user locks a clip mid-cycle the dashcam moves it + from /DCIM/Movie to /DCIM/Movie/RO. The next reconcile must + refresh source_dir on the existing row, otherwise the + download worker keeps hitting the stale path and 404s out + its retry budget.""" + provider = MagicMock() + provider.get.return_value = _make_snap() + hub = Hub() + worker = SyncWorker(db, provider, hub) + + with patch.object(worker, "_fetch_listing", + return_value=[_Rec("A.MP4", filepath="/DCIM/Movie")]), \ + patch.object(worker, "_present_filenames", return_value=[]): + await worker._refresh_listing_and_reconcile() + + with db.conn() as c: + row = c.execute( + "SELECT source_dir FROM download_queue WHERE filename='A.MP4'" + ).fetchone() + assert row["source_dir"] == "/DCIM/Movie" + + # User locks the clip; dashcam re-reports it under /RO. + with patch.object(worker, "_fetch_listing", + return_value=[_Rec("A.MP4", filepath="/DCIM/Movie/RO")]), \ + patch.object(worker, "_present_filenames", return_value=[]): + await worker._refresh_listing_and_reconcile() + + with db.conn() as c: + row = c.execute( + "SELECT source_dir FROM download_queue WHERE filename='A.MP4'" + ).fetchone() + assert row["source_dir"] == "/DCIM/Movie/RO" + + async def test_refresh_filters_by_ro_only_when_setting_on( db: Database, ) -> None: diff --git a/web/services/queue.py b/web/services/queue.py index 7b7f810..a0d1e8e 100644 --- a/web/services/queue.py +++ b/web/services/queue.py @@ -66,11 +66,12 @@ def reconcile( added = 0 marked_gone = 0 marked_done = 0 + refreshed_source_dir = 0 with db.write() as c: existing = { row["filename"]: dict(row) for row in c.execute( - "SELECT filename, state FROM download_queue" + "SELECT filename, state, source_dir FROM download_queue" ).fetchall() } @@ -115,6 +116,22 @@ def reconcile( continue if filename in existing: + # Locking a clip on the dashcam moves it from + # /DCIM/Movie to /DCIM/Movie/RO. The fresh listing + # carries the new path; refresh the queue row so + # the worker doesn't keep retrying the stale URL. + fresh_source = getattr(rec, "filepath", "") or "" + if ( + fresh_source + and existing[filename]["source_dir"] != fresh_source + and existing[filename]["state"] in ("pending", "failed") + ): + c.execute( + "UPDATE download_queue SET source_dir=? " + "WHERE filename=?", + (fresh_source, filename), + ) + refreshed_source_dir += 1 continue c.execute( @@ -158,6 +175,7 @@ def reconcile( "added": added, "marked_gone": marked_gone, "marked_done": marked_done, + "refreshed_source_dir": refreshed_source_dir, }