|
52 | 52 | assert_rom_visible, |
53 | 53 | get_permissions, |
54 | 54 | ) |
55 | | -from handler.database import db_rom_handler, db_save_handler |
| 55 | +from handler.database import db_collection_handler, db_rom_handler, db_save_handler |
56 | 56 | from handler.database.base_handler import sync_session |
57 | 57 | from handler.filesystem import fs_resource_handler, fs_rom_handler |
58 | 58 | from handler.filesystem.assets_handler import validate_image_upload |
|
112 | 112 | router.include_router(patch_router) |
113 | 113 |
|
114 | 114 |
|
| 115 | +# RomUser fields the statuses filter branches on. |
| 116 | +STATUS_MEMBERSHIP_FIELDS = frozenset({"status", "now_playing", "backlogged", "hidden"}) |
| 117 | + |
| 118 | + |
115 | 119 | def safe_int_or_none(value: Any) -> int | None: |
116 | 120 | if value is None or value == "": |
117 | 121 | return None |
118 | 122 |
|
119 | 123 | return safe_int(value) |
120 | 124 |
|
121 | 125 |
|
| 126 | +def refresh_affected_smart_collections( |
| 127 | + rom_ids: Sequence[int], membership_only: bool = False |
| 128 | +) -> None: |
| 129 | + """Follow a change into the cached smart collection membership. |
| 130 | +
|
| 131 | + The write has already been committed, so a stale count is the worst this |
| 132 | + can cost, and reporting it back as a failed write would be a lie. |
| 133 | + """ |
| 134 | + try: |
| 135 | + db_collection_handler.refresh_smart_collections_for_roms( |
| 136 | + rom_ids, membership_only=membership_only |
| 137 | + ) |
| 138 | + except Exception as e: |
| 139 | + log.error(f"Couldn't refresh smart collections for {rom_ids}: {e}") |
| 140 | + |
| 141 | + |
122 | 142 | def build_unscoped_sidecar_cache_key( |
123 | 143 | user_id: int, |
124 | 144 | order_by: str, |
@@ -613,8 +633,8 @@ def get_roms( |
613 | 633 | query = db_rom_handler.filter_roms( |
614 | 634 | query=unfiltered_query, |
615 | 635 | user_id=request.user.id, |
616 | | - hidden_platform_ids=perms.hidden_platform_ids, |
617 | | - hidden_rom_ids=perms.hidden_rom_ids, |
| 636 | + hidden_platform_ids=perms.hidden_platform_ids, # type: ignore |
| 637 | + hidden_rom_ids=perms.hidden_rom_ids, # type: ignore |
618 | 638 | platform_ids=platform_ids, |
619 | 639 | collection_id=collection_id, |
620 | 640 | virtual_collection_id=virtual_collection_id, |
@@ -1536,6 +1556,7 @@ async def update_rom( |
1536 | 1556 | raise RomNotFoundInDatabaseException(id) |
1537 | 1557 |
|
1538 | 1558 | db_rom_handler.invalidate_filter_values_cache() |
| 1559 | + refresh_affected_smart_collections([id]) |
1539 | 1560 | return DetailedRomSchema.from_orm_with_request(rom, request) |
1540 | 1561 |
|
1541 | 1562 | provided_fields = form_data.model_fields_set |
@@ -1915,6 +1936,7 @@ async def update_rom( |
1915 | 1936 | fire_and_forget(meta_playmatch_handler.submit_manual_match_suggestion(rom)) |
1916 | 1937 |
|
1917 | 1938 | db_rom_handler.invalidate_filter_values_cache() |
| 1939 | + refresh_affected_smart_collections([id]) |
1918 | 1940 | return DetailedRomSchema.from_orm_with_request(rom, request) |
1919 | 1941 |
|
1920 | 1942 |
|
@@ -1981,7 +2003,7 @@ async def delete_roms( |
1981 | 2003 | perms = get_permissions(request) |
1982 | 2004 | assert_can(perms, PermEntity.ROMS, PermAction.DELETE) |
1983 | 2005 |
|
1984 | | - successful_items = 0 |
| 2006 | + deleted_ids: list[int] = [] |
1985 | 2007 | failed_ids = [] |
1986 | 2008 | errors = [] |
1987 | 2009 |
|
@@ -2036,16 +2058,19 @@ async def delete_roms( |
2036 | 2058 | f"Couldn't find resources to delete for {hl(str(rom.name or 'ROM'), color=BLUE)}" |
2037 | 2059 | ) |
2038 | 2060 |
|
2039 | | - successful_items += 1 |
| 2061 | + deleted_ids.append(id) |
2040 | 2062 | except Exception as e: |
2041 | 2063 | failed_ids.append(id) |
2042 | 2064 | errors.append(f"Failed to delete ROM {id}: {str(e)}") |
2043 | 2065 |
|
2044 | | - if successful_items: |
| 2066 | + if deleted_ids: |
2045 | 2067 | db_rom_handler.invalidate_filter_values_cache() |
| 2068 | + # Deleted ROMs would otherwise linger in the cached smart collection |
| 2069 | + # membership until the next scan. |
| 2070 | + refresh_affected_smart_collections(deleted_ids) |
2046 | 2071 |
|
2047 | 2072 | return { |
2048 | | - "successful_items": successful_items, |
| 2073 | + "successful_items": len(deleted_ids), |
2049 | 2074 | "failed_ids": failed_ids, |
2050 | 2075 | "errors": errors, |
2051 | 2076 | } |
@@ -2098,4 +2123,9 @@ async def update_rom_user( |
2098 | 2123 | if "hidden" in cleaned_data: |
2099 | 2124 | db_rom_handler.invalidate_filter_values_cache() |
2100 | 2125 |
|
| 2126 | + # The statuses filter reads all four of these, and `hidden` also drops the |
| 2127 | + # ROM from every user-scoped query, so any of them can move membership. |
| 2128 | + if STATUS_MEMBERSHIP_FIELDS & cleaned_data.keys(): |
| 2129 | + refresh_affected_smart_collections([id], membership_only=True) |
| 2130 | + |
2101 | 2131 | return RomUserSchema.model_validate(rom_user) |
0 commit comments