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, }