Media Library Change Tracking#
Overview#
Screenbox uses the Windows StorageLibrary.ChangeTracker API to avoid full re-scans on every launch. When a valid cache exists and the library's folder composition hasn't changed, only the delta of file-level changes (additions, deletions, renames, content updates) is applied against the cached media list. The key components are:
LibraryServiceβ orchestrates change detection and cache reconciliationDatabaseService.LibraryCacheβ persists and loads library folder paths and media recordsDatabaseService.Schemaβ defineslibrary_foldersandmedia_recordstables
How Change Tracking Works#
Initialization#
Both InitializeMusicLibraryAsync and InitializeVideosLibraryAsync call StorageLibrary.GetLibraryAsync and then library.ChangeTracker.Enable(). Exceptions from Enable() are silently swallowed so the app degrades gracefully if the platform doesn't support the API.
Cache Validation β Folder Path Check#
On each fetch (FetchMusicAsync / FetchVideosAsync), the cache is considered stale if the current library folder set doesn't match what was saved. AreLibraryPathsChanged compares the count and paths of library.Folders against the persisted library_folders rows. Any mismatch forces a full rescan instead of incremental reconciliation.
Cache is also unconditionally disabled on Xbox (useCache = useCache && !SystemInformation.IsXbox) .
Incremental Change Resolution#
When the folder set is unchanged, TryResolveLibraryChangeAsync is called:
- If
GetLastChangeId()is available (Win10 1803+) and returnsUnknown, the tracker has overflowed β falls back to full rescan. - If
changeId == 0, no changes exist β cache is used as-is. - Otherwise,
TryResolveLibraryBatchChangeAsyncprocesses the batch.
TryResolveLibraryBatchChangeAsync iterates changeReader.ReadBatchAsync() and handles these change types in-memory:
| Change Type | Action |
|---|---|
Created, MovedIntoLibrary | Adds a new MediaViewModel to the list |
Deleted, MovedOutOfLibrary | Removes the matching entry by PreviousPath |
MovedOrRenamed | Removes old entry, adds new one (preserving MediaInfo) |
ContentsChanged, ContentsReplaced | Calls UpdateSource() on the existing entry |
EncryptionChanged, IndexingStatusChanged | Ignored |
ChangeTrackingLost | Returns false β triggers full rescan |
| Any folder change (non-indexing/encryption) | Returns false β triggers full rescan |
Accepting or Resetting Changes#
After a successful incremental pass, changeReader.AcceptChangesAsync() advances the tracker's checkpoint . After a full rescan, libraryChangeTracker.Reset() clears all pending changes so the next launch starts fresh .
Cache Persistence#
DatabaseService.LibraryCache stores two kinds of data per media type:
library_foldersβ one row per folder path per media type; fully replaced on each save (DELETE then INSERT)media_recordsβ upserted withINSERT OR REPLACEkeyed onpath
LoadLibraryCacheAsync reads both tables in a single connection and returns a RawCacheLoadResultDto containing folder paths and raw media records.
Known Gap: Orphaned Records on Folder Changes#
When a folder change triggers a full rescan, media_records rows for files in the old folder set are never explicitly deleted. The save path (CacheSongsAsync / CacheVideosAsync) uses INSERT OR REPLACE, which only upserts the current scan result. Records for deleted or moved files remain in the media_records table until the path appears again or a schema drop-and-recreate occurs.
This means after a folder removal + full rescan, the media_records table may contain orphaned rows (files no longer in any library folder). These orphans are invisible at runtime β LoadLibraryCacheAsync returns all records regardless of whether their paths still exist β but they consume space and can cause unexpected behavior if path collisions occur with future additions.
External Events and Debouncing#
ContentsChanged events from StorageFileQueryResult are debounced by 1 second in LibraryCoordinator before a re-fetch is triggered. The StorageLibrary.DefinitionChanged event (folder additions/removals) is intentionally not subscribed to β the comment in InitializeMusicLibraryAsync notes "No need to add handler for StorageLibrary.DefinitionChanged" . Folder changes are instead detected lazily on the next fetch via AreLibraryPathsChanged.
Key Files#
| File | Purpose |
|---|---|
LibraryService.cs | Change tracking logic, cache fetch/save orchestration |
DatabaseService.LibraryCache.cs | SQL queries for library_folders and media_records |
DatabaseService.Schema.cs | Table definitions and schema migration |