From 191f3039bf9cf2d7ecb7db06d396981c2ccdb5f2 Mon Sep 17 00:00:00 2001 From: Roland Date: Mon, 14 Sep 2026 18:51:06 -0600 Subject: [PATCH] files: direct file refresh skips ignored directories Refreshing a specific file (the Files page Refresh button) bypassed the ignored_directories check that directory and global refreshes apply. Refreshing a hard link inside the playlists directory therefore created a second Video for the same file. The direct-file path now drops existing files that live in an ignored directory before comparing against the database. Deleted paths are kept so stale records are still removed. Co-Authored-By: Claude Fable 5.1 --- wrolpi/files/test/test_worker.py | 62 ++++++++++++++++++++++++++++++++ wrolpi/files/worker.py | 8 ++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/wrolpi/files/test/test_worker.py b/wrolpi/files/test/test_worker.py index 2c721ffb..74b90f05 100644 --- a/wrolpi/files/test/test_worker.py +++ b/wrolpi/files/test/test_worker.py @@ -1804,3 +1804,65 @@ async def test_refresh_sync_skips_post_processing_when_unchanged(async_client, t file_worker._apply_post_processing = original assert called is False, '_apply_post_processing should be skipped when no files changed' + + +@pytest.mark.asyncio +async def test_direct_file_refresh_skips_ignored_directory( + async_client, test_session, test_directory, make_files_structure, test_wrolpi_config, await_background_tasks +): + """Refreshing a file directly must not index it when it lives in an ignored directory. + + The playlists directory holds hard links of indexed videos. Indexing a link would create a second Video for the + same file. + """ + from wrolpi.common import get_wrolpi_config + + get_wrolpi_config().ignored_directories = ['config', 'tags', 'playlists'] + await await_background_tasks() + + video, link = make_files_structure([ + 'videos/channel/video.mp4', + 'playlists/Testing/0001_video.mp4', + ]) + + task = FileTask(FileTaskType.refresh, [video, link]) + file_worker.private_queue.put_nowait(task) + await file_worker.process_queue() + + test_session.expire_all() + primary_paths = {str(i.primary_path) for i in test_session.query(FileGroup)} + assert primary_paths == {str(video)} + + +@pytest.mark.asyncio +async def test_directory_refresh_deletes_stale_ignored_file_groups( + async_client, test_session, test_directory, make_files_structure, test_wrolpi_config, await_background_tasks +): + """A FileGroup that was indexed before its directory was ignored is removed by the next refresh.""" + from wrolpi.common import get_wrolpi_config + + get_wrolpi_config().ignored_directories = ['config', 'tags'] + await await_background_tasks() + + video, link = make_files_structure([ + 'videos/channel/video.mp4', + 'playlists/Testing/0001_video.mp4', + ]) + + task = FileTask(FileTaskType.refresh, [test_directory], count=2) + file_worker.private_queue.put_nowait(task) + await file_worker.process_queue() + test_session.expire_all() + assert test_session.query(FileGroup).count() == 2 + + # The user (or WROLPi) ignores the playlists directory, the stale record must go away. + get_wrolpi_config().ignored_directories = ['config', 'tags', 'playlists'] + await await_background_tasks() + + task = FileTask(FileTaskType.refresh, [test_directory], count=1) + file_worker.private_queue.put_nowait(task) + await file_worker.process_queue() + + test_session.expire_all() + primary_paths = {str(i.primary_path) for i in test_session.query(FileGroup)} + assert primary_paths == {str(video)} diff --git a/wrolpi/files/worker.py b/wrolpi/files/worker.py index bbd34721..e662f86a 100644 --- a/wrolpi/files/worker.py +++ b/wrolpi/files/worker.py @@ -36,7 +36,7 @@ from wrolpi.files.lib import ( split_path_stem_and_suffix, _upsert_files, get_unique_files_by_stem, glob_shared_stem, group_files_by_stem, get_primary_file, delete_directory, apply_indexers, _move_file_group_files, _bulk_update_file_groups_db, MOVE_CHUNK_SIZE, - _bulk_update_file_groups_reorganize, get_normalized_ignored_directories, + _bulk_update_file_groups_reorganize, get_normalized_ignored_directories, remove_files_in_ignored_directories, ) logger = logger.getChild(__name__) @@ -1849,6 +1849,12 @@ class FileWorker: else: all_fs_files.add(file_path) + # Never index files in ignored directories (playlists/tags hold hard links of already-indexed files, which + # would otherwise become duplicate FileGroups). Deleted paths are kept so stale records are still removed. + existing_files = [i for i in all_fs_files if i.exists()] + allowed_files = set(remove_files_in_ignored_directories(existing_files)) + all_fs_files = {i for i in all_fs_files if not i.exists() or i in allowed_files} + if not all_fs_files: return FileComparisonResult(unchanged=[], new=[], deleted=[], modified=[])