[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