[vlc-commits] [Git][videolan/vlc][master] 2 commits: macosx: Refresh library caches when updates affect filtering or sorting

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Fri Oct 9 12:35:28 UTC 2026



Felix Paul Kühne pushed to branch master at VideoLAN / VLC


Commits:
e2507407 by Claudio Cambra at 2026-10-09T14:10:22+02:00
macosx: Refresh library caches when updates affect filtering or sorting

Re-query affected caches when item updates can change filter membership or sort order, rather than replacing items at their existing positions

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
42811092 by Claudio Cambra at 2026-10-09T14:10:22+02:00
macosx: More granularly decide whether full cache reloads are necessary

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -


3 changed files:

- modules/gui/macosx/library/VLCLibraryDataTypes.h
- modules/gui/macosx/library/VLCLibraryDataTypes.m
- modules/gui/macosx/library/VLCLibraryModel.m


Changes:

=====================================
modules/gui/macosx/library/VLCLibraryDataTypes.h
=====================================
@@ -185,6 +185,7 @@ typedef NS_ENUM(NSUInteger, VLCMediaLibraryParentGroupType) {
 @property (readonly) NSString *shortBiography;
 @property (readonly) NSString *musicBrainzID;
 @property (readonly) unsigned int numberOfAlbums;
+ at property (readonly) uint32_t numberOfPresentTracks;
 
 @end
 
@@ -200,6 +201,7 @@ typedef NS_ENUM(NSUInteger, VLCMediaLibraryParentGroupType) {
 @property (readonly) int64_t artistID;
 @property (readonly) int64_t duration;
 @property (readonly) unsigned int year;
+ at property (readonly) uint32_t numberOfPresentTracks;
 
 @end
 


=====================================
modules/gui/macosx/library/VLCLibraryDataTypes.m
=====================================
@@ -537,6 +537,7 @@ static NSString *genreArrayDisplayString(NSArray<VLCMediaLibraryGenre *> * const
         _musicBrainzID = toNSStr(p_artist->psz_mb_id);
         _numberOfAlbums = p_artist->i_nb_album;
         _numberOfTracks = p_artist->i_nb_tracks;
+        _numberOfPresentTracks = p_artist->i_nb_present_tracks;
         _favorited = p_artist->b_is_favorite;
     }
     return self;
@@ -676,6 +677,7 @@ static NSString *genreArrayDisplayString(NSArray<VLCMediaLibraryGenre *> * const
         _artistName = toNSStr(p_album->psz_artist);
         _artistID = p_album->i_artist_id;
         _numberOfTracks = p_album->i_nb_tracks;
+        _numberOfPresentTracks = p_album->i_nb_present_tracks;
         _duration = p_album->i_duration;
         _year = p_album->i_year;
         _favorited = p_album->b_is_favorite;


=====================================
modules/gui/macosx/library/VLCLibraryModel.m
=====================================
@@ -109,6 +109,13 @@ NSString * const VLCLibraryModelDiscoveryFailed = @"VLCLibraryModelDiscoveryFail
     NSCountedSet<NSString *> *_mediaTitleCounts;
     NSMutableDictionary<NSValue *, NSObject *> *_cacheUpdateTokens;
     NSMutableSet<NSValue *> *_pendingCacheUpdates;
+    // Owned by each cache's modification queue. Later updates must preserve a
+    // pending membership/order refresh even when they change unrelated fields.
+    BOOL _audioCacheNeedsReload;
+    BOOL _videoCacheNeedsReload;
+    BOOL _artistCacheNeedsReload;
+    BOOL _albumCacheNeedsReload;
+    BOOL _genreCacheNeedsReload;
 }
 
 @property (readwrite) NSArray<VLCMediaLibraryFolderObserver *> *folderObservers;
@@ -489,6 +496,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
     dispatch_barrier_async(_mediaItemCacheModificationQueue, ^{
         [self replaceMediaTitlesFromMedia:self->_cachedAudioMedia withMedia:media];
         self->_cachedAudioMedia = media;
+        self->_audioCacheNeedsReload = NO;
     });
 }
 
@@ -499,6 +507,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
     dispatch_barrier_async(_mediaItemCacheModificationQueue, ^{
         [self replaceMediaTitlesFromMedia:self->_cachedVideoMedia withMedia:media];
         self->_cachedVideoMedia = media;
+        self->_videoCacheNeedsReload = NO;
     });
 }
 
@@ -547,6 +556,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
     [self invalidateCacheUpdateForGetter:@selector(cachedAlbums)];
     dispatch_barrier_async(_albumCacheModificationQueue, ^{
         self->_cachedAlbums = [cachedAlbums copy];
+        self->_albumCacheNeedsReload = NO;
     });
 }
 
@@ -555,6 +565,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
     [self invalidateCacheUpdateForGetter:@selector(cachedArtists)];
     dispatch_barrier_async(_artistCacheModificationQueue, ^{
         self->_cachedArtists = [cachedArtists copy];
+        self->_artistCacheNeedsReload = NO;
     });
 }
 
@@ -563,6 +574,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
     [self invalidateCacheUpdateForGetter:@selector(cachedGenres)];
     dispatch_barrier_async(_genreCacheModificationQueue, ^{
         self->_cachedGenres = [cachedGenres copy];
+        self->_genreCacheNeedsReload = NO;
     });
 }
 
@@ -729,6 +741,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                                        block:^{
             [self replaceMediaTitlesFromMedia:self->_cachedAudioMedia withMedia:mediaArray];
             self->_cachedAudioMedia = mediaArray;
+            self->_audioCacheNeedsReload = NO;
             [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
                 [self.changeDelegate notifyChange:VLCLibraryModelAudioMediaListReset withObject:self];
             }];
@@ -785,6 +798,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                                      onQueue:self->_artistCacheModificationQueue
                                        block:^{
             self->_cachedArtists = mutableArtistArray.copy;
+            self->_artistCacheNeedsReload = NO;
             [self performAfterCacheWritesOnQueue:self->_artistCacheModificationQueue block:^{
                 if (![self isCurrentCacheUpdate:token forGetter:@selector(cachedArtists)]) {
                     return;
@@ -841,6 +855,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                                      onQueue:self->_albumCacheModificationQueue
                                        block:^{
             self->_cachedAlbums = mutableAlbumArray.copy;
+            self->_albumCacheNeedsReload = NO;
             [self performAfterCacheWritesOnQueue:self->_albumCacheModificationQueue block:^{
                 if (![self isCurrentCacheUpdate:token forGetter:@selector(cachedAlbums)]) {
                     return;
@@ -897,6 +912,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                                      onQueue:self->_genreCacheModificationQueue
                                        block:^{
             self->_cachedGenres = mutableGenreArray.copy;
+            self->_genreCacheNeedsReload = NO;
             [self performAfterCacheWritesOnQueue:self->_genreCacheModificationQueue block:^{
                 if (![self isCurrentCacheUpdate:token forGetter:@selector(cachedGenres)]) {
                     return;
@@ -957,6 +973,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
             NSArray<VLCMediaLibraryMediaItem *> * const media = mutableArray.copy;
             [self replaceMediaTitlesFromMedia:self->_cachedVideoMedia withMedia:media];
             self->_cachedVideoMedia = media;
+            self->_videoCacheNeedsReload = NO;
             [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
                 [self.changeDelegate notifyChange:VLCLibraryModelVideoMediaListReset withObject:self];
             }];
@@ -1659,6 +1676,89 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
     });
 }
 
+- (BOOL)shouldReloadCacheForUpdatedItem:(id<VLCMediaLibraryItemProtocol>)item
+                           previousItem:(id<VLCMediaLibraryItemProtocol>)previousItem
+{
+    if (previousItem == nil || item.class != previousItem.class ||
+        item.libraryID != previousItem.libraryID)
+        return YES;
+
+    if ([item isKindOfClass:VLCMediaLibraryGenre.class]) {
+        // Genre queries search and sort by name, regardless of sort criterion.
+        return ![((VLCMediaLibraryGenre *)item).name isEqualToString:((VLCMediaLibraryGenre *)previousItem).name];
+    }
+
+    if ([item isKindOfClass:VLCMediaLibraryArtist.class]) {
+        VLCMediaLibraryArtist * const artist = (id)item;
+        VLCMediaLibraryArtist * const previousArtist = (id)previousItem;
+        if (![artist.name isEqualToString:previousArtist.name] ||
+            (artist.numberOfPresentTracks > 0) != (previousArtist.numberOfPresentTracks > 0) ||
+            (artist.numberOfTracks > 0) != (previousArtist.numberOfTracks > 0))
+            return YES;
+        switch (_sortCriteria) {
+            case VLC_ML_SORTING_DEFAULT:
+            case VLC_ML_SORTING_ALPHA:
+                return NO;
+            case VLC_ML_SORTING_TRACKNUMBER:
+                return artist.numberOfTracks != previousArtist.numberOfTracks;
+            default:
+                return YES;
+        }
+    }
+
+    if ([item isKindOfClass:VLCMediaLibraryAlbum.class]) {
+        VLCMediaLibraryAlbum * const album = (id)item;
+        VLCMediaLibraryAlbum * const previousAlbum = (id)previousItem;
+        if (![album.title isEqualToString:previousAlbum.title] ||
+            (album.numberOfPresentTracks > 0) != (previousAlbum.numberOfPresentTracks > 0) ||
+            (_filterString.length > 0 && ![album.artistName isEqualToString:previousAlbum.artistName]))
+            return YES;
+        switch (_sortCriteria) {
+            case VLC_ML_SORTING_DEFAULT:
+            case VLC_ML_SORTING_ALPHA:
+                return ![album.artistName isEqualToString:previousAlbum.artistName];
+            case VLC_ML_SORTING_DURATION:
+                return album.duration != previousAlbum.duration;
+            case VLC_ML_SORTING_RELEASEDATE:
+                return album.year != previousAlbum.year;
+            case VLC_ML_SORTING_ARTIST:
+                return ![album.artistName isEqualToString:previousAlbum.artistName];
+            case VLC_ML_SORTING_TRACKNUMBER:
+                return album.numberOfTracks != previousAlbum.numberOfTracks;
+            default:
+                // Aggregate play counts and insertion dates are not in the snapshot.
+                return YES;
+        }
+    }
+
+    if ([item isKindOfClass:VLCMediaLibraryGroup.class]) {
+        VLCMediaLibraryGroup * const group = (id)item;
+        VLCMediaLibraryGroup * const previousGroup = (id)previousItem;
+        if (![group.name isEqualToString:previousGroup.name] ||
+            (group.numberOfPresentTotalItems > 0) != (previousGroup.numberOfPresentTotalItems > 0))
+            return YES;
+        switch (_sortCriteria) {
+            case VLC_ML_SORTING_DEFAULT:
+            case VLC_ML_SORTING_ALPHA:
+                return NO;
+            case VLC_ML_SORTING_DURATION:
+                return group.duration != previousGroup.duration;
+            case VLC_ML_SORTING_INSERTIONDATE:
+                return ![group.creationDate isEqualToDate:previousGroup.creationDate];
+            case VLC_ML_SORTING_LASTMODIFICATIONDATE:
+                return ![group.lastModificationDate isEqualToDate:previousGroup.lastModificationDate];
+            default:
+                return YES;
+        }
+    }
+
+    // Media labels are lazy lookups, not snapshots of the previous search data.
+    // Retain full queries where membership or sort keys cannot be compared safely.
+    return _filterString.length > 0 ||
+           (_sortCriteria != VLC_ML_SORTING_DEFAULT && _sortCriteria != VLC_ML_SORTING_ALPHA) ||
+           ![previousItem.displayString isEqualToString:item.displayString];
+}
+
 - (void)handleMediaItemUpdateEvent:(const vlc_ml_event_t * const)p_event
 {
     NSParameterAssert(p_event != NULL);
@@ -1680,19 +1780,44 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
         const NSUInteger showIndex,
         const NSUInteger __unused showEpisodeIndex
     ) {
+        id<VLCMediaLibraryItemProtocol> const previousItem =
+            cachedMediaIndex == NSNotFound ? nil : cachedMediaArray[cachedMediaIndex];
+        const BOOL audio = mediaItem.mediaType == VLC_ML_MEDIA_TYPE_AUDIO;
+        NSArray * const mainCache = audio ? self->_cachedAudioMedia : self->_cachedVideoMedia;
+        NSArray * const recentCache = audio ? self->_cachedRecentAudioMedia : self->_cachedRecentMedia;
+        const BOOL reloadMain = mainCache != nil &&
+            ((audio ? self->_audioCacheNeedsReload : self->_videoCacheNeedsReload) ||
+             [self shouldReloadCacheForUpdatedItem:mediaItem previousItem:previousItem]);
+        const BOOL reloadRecent = recentCache != nil && self->_filterString.length > 0;
+        if (reloadMain) {
+            if (audio) {
+                self->_audioCacheNeedsReload = YES;
+                [self resetCachedListOfAudioMedia];
+            } else {
+                self->_videoCacheNeedsReload = YES;
+                [self resetCachedListOfVideoMedia];
+            }
+        }
+        if (reloadRecent) {
+            if (audio)
+                [self resetCachedListOfRecentAudioMedia];
+            else
+                [self resetCachedListOfRecentMedia];
+        }
         if (cachedMediaIndex == NSNotFound && recentMediaIndex == NSNotFound) {
-            NSLog(@"Could not handle update for media library item with id %lld in model", itemId);
+            if (!reloadMain && !reloadRecent)
+                NSLog(@"Could not handle update for media library item with id %lld in model", itemId);
             return;
         }
 
         // Notify what happened
-        if (cachedMediaIndex != NSNotFound) {
+        if (cachedMediaIndex != NSNotFound && !reloadMain) {
             [self replaceMediaTitle:((VLCMediaLibraryMediaItem *)cachedMediaArray[cachedMediaIndex]).displayString
                           withTitle:mediaItem.displayString];
             [cachedMediaArray replaceObjectAtIndex:cachedMediaIndex withObject:mediaItem];
         }
 
-        if (recentMediaArray != nil && recentMediaIndex != NSNotFound) {
+        if (recentMediaArray != nil && recentMediaIndex != NSNotFound && !reloadRecent) {
             [recentMediaArray replaceObjectAtIndex:recentMediaIndex withObject:mediaItem];
             [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
                 switch (mediaItem.mediaType) {
@@ -1721,6 +1846,9 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
             }];
         }
 
+        if (reloadMain) {
+            return;
+        }
         [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
             switch (mediaItem.mediaType) {
                 case VLC_ML_MEDIA_TYPE_VIDEO:
@@ -1755,6 +1883,13 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
         const NSUInteger __unused showEpisodeIndex
     ) {
 
+        // A deletion also supersedes any snapshot fetched before it. Preserve
+        // pending membership/order changes by querying the latest state again.
+        if (self->_audioCacheNeedsReload && self->_cachedAudioMedia != nil)
+            [self resetCachedListOfAudioMedia];
+        if (self->_videoCacheNeedsReload && self->_cachedVideoMedia != nil)
+            [self resetCachedListOfVideoMedia];
+
         if (cachedMediaIndex == NSNotFound && recentMediaIndex == NSNotFound) {
             NSLog(@"Could not handle deletion for media library item with id %lld in model", itemId);
             return;
@@ -1840,11 +1975,15 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                  usingGetter:(NSArray *(^)(void))cacheGetter
                     forCache:(SEL)getter
                  usingSetter:(void (^)(NSArray *))cacheSetter
+                usingRefresh:(dispatch_block_t)cacheRefresh
+               reloadPending:(BOOL *)reloadPending
                   usingQueue:(const dispatch_queue_t)queue
         withNotificationName:(const NSNotificationName)notificationName
 {
     NSParameterAssert(cacheGetter != nil);
     NSParameterAssert(cacheSetter != nil);
+    NSParameterAssert(cacheRefresh != nil);
+    NSParameterAssert(reloadPending != NULL);
     const int64_t itemId = audioGroupItem.libraryID;
 
     dispatch_barrier_async(queue, ^{
@@ -1856,8 +1995,11 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
 
         const NSUInteger audioGroupIndex = [self indexForAudioGroupInCache:cache
                                                                 withItemId:itemId];
-        if (audioGroupIndex == NSNotFound) {
-            NSLog(@"Did not find audio group item with id %lli in cache.", itemId);
+        id<VLCMediaLibraryItemProtocol> const previousItem =
+            audioGroupIndex == NSNotFound ? nil : cache[audioGroupIndex];
+        if (*reloadPending || [self shouldReloadCacheForUpdatedItem:audioGroupItem previousItem:previousItem]) {
+            *reloadPending = YES;
+            cacheRefresh();
             return;
         }
 
@@ -1881,11 +2023,15 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                        usingGetter:(NSArray *(^)(void))cacheGetter
                           forCache:(SEL)getter
                        usingSetter:(void (^)(NSArray *))cacheSetter
+                      usingRefresh:(dispatch_block_t)cacheRefresh
+                     reloadPending:(BOOL *)reloadPending
                         usingQueue:(const dispatch_queue_t)queue
               withNotificationName:(const NSNotificationName)notificationName
 {
     NSParameterAssert(cacheGetter != nil);
     NSParameterAssert(cacheSetter != nil);
+    NSParameterAssert(cacheRefresh != nil);
+    NSParameterAssert(reloadPending != NULL);
 
     dispatch_barrier_async(queue, ^{
         NSArray * const cache = cacheGetter();
@@ -1894,6 +2040,11 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
             return;
         }
 
+        if (*reloadPending) {
+            cacheRefresh();
+            return;
+        }
+
         const NSUInteger audioGroupIndex = [self indexForAudioGroupInCache:cache
                                                                 withItemId:itemId];
         if (audioGroupIndex == NSNotFound) {
@@ -1935,6 +2086,8 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                    usingSetter:^(NSArray *cache) {
         self->_cachedAlbums = cache;
     }
+                  usingRefresh:^{ [self resetCachedListOfAlbums]; }
+                 reloadPending:&_albumCacheNeedsReload
                     usingQueue:_albumCacheModificationQueue
           withNotificationName:VLCLibraryModelAlbumUpdated];
 }
@@ -1951,6 +2104,8 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                          usingSetter:^(NSArray *cache) {
         self->_cachedAlbums = cache;
     }
+                        usingRefresh:^{ [self resetCachedListOfAlbums]; }
+                       reloadPending:&_albumCacheNeedsReload
                           usingQueue:_albumCacheModificationQueue
                 withNotificationName:VLCLibraryModelAlbumDeleted];
 }
@@ -1973,6 +2128,8 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                    usingSetter:^(NSArray *cache) {
         self->_cachedArtists = cache;
     }
+                  usingRefresh:^{ [self resetCachedListOfArtists]; }
+                 reloadPending:&_artistCacheNeedsReload
                     usingQueue:_artistCacheModificationQueue
           withNotificationName:VLCLibraryModelArtistUpdated];
 }
@@ -1989,6 +2146,8 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                          usingSetter:^(NSArray *cache) {
         self->_cachedArtists = cache;
     }
+                        usingRefresh:^{ [self resetCachedListOfArtists]; }
+                       reloadPending:&_artistCacheNeedsReload
                           usingQueue:_artistCacheModificationQueue
                 withNotificationName:VLCLibraryModelArtistDeleted];
 }
@@ -2011,6 +2170,8 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                    usingSetter:^(NSArray *cache) {
         self->_cachedGenres = cache;
     }
+                  usingRefresh:^{ [self resetCachedListOfGenres]; }
+                 reloadPending:&_genreCacheNeedsReload
                     usingQueue:_genreCacheModificationQueue
           withNotificationName:VLCLibraryModelGenreUpdated];
 }
@@ -2027,6 +2188,8 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                          usingSetter:^(NSArray *cache) {
         self->_cachedGenres = cache;
     }
+                        usingRefresh:^{ [self resetCachedListOfGenres]; }
+                       reloadPending:&_genreCacheNeedsReload
                           usingQueue:_genreCacheModificationQueue
                 withNotificationName:VLCLibraryModelGenreDeleted];
 }



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/8782d43c1f17de497f3b25c690be5ba0ded35d98...428110923693cd391229f8b6afce4e61debc04f0

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/8782d43c1f17de497f3b25c690be5ba0ded35d98...428110923693cd391229f8b6afce4e61debc04f0
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list