[vlc-commits] [Git][videolan/vlc][master] macosx: Correctly update shows when episode media items change

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Sun Oct 11 05:47:17 UTC 2026



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


Commits:
cb254da4 by Claudio Cambra at 2026-10-11T07:19:48+02:00
macosx: Correctly update shows when episode media items change

Retain an episode->show mapping to granularly update shows' episode data upon item changes. Also make sure overlapping changes are coalesced into a single full reconciliation fetch, stale results rejected using a similar (but per-show) token system as with other media item caches

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

- - - - -


1 changed file:

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


Changes:

=====================================
modules/gui/macosx/library/VLCLibraryModel.m
=====================================
@@ -109,6 +109,10 @@ NSString * const VLCLibraryModelDiscoveryFailed = @"VLCLibraryModelDiscoveryFail
     NSCountedSet<NSString *> *_mediaTitleCounts;
     NSMutableDictionary<NSValue *, NSObject *> *_cacheUpdateTokens;
     NSMutableSet<NSValue *> *_pendingCacheUpdates;
+    // Owned by the media-cache queue. Preserve membership after the ML removes
+    // a relationship, even if an earlier media update has cleared the subtype.
+    NSMutableDictionary<NSNumber *, NSNumber *> *_showIDsByEpisodeID;
+    NSMutableDictionary<NSNumber *, NSObject *> *_showRefreshTokens;
     // Owned by each cache's modification queue. Later updates must preserve a
     // pending membership/order refresh even when they change unrelated fields.
     BOOL _audioCacheNeedsReload;
@@ -345,6 +349,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
         _mediaTitleCounts = [NSCountedSet new];
         _cacheUpdateTokens = NSMutableDictionary.dictionary;
         _pendingCacheUpdates = NSMutableSet.set;
+        _showRefreshTokens = NSMutableDictionary.dictionary;
 
         _defaultNotificationCenter = NSNotificationCenter.defaultCenter;
         [_defaultNotificationCenter addObserver:self
@@ -527,11 +532,57 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
     });
 }
 
-- (void)setCachedListOfShows:(NSArray *)cachedListOfShows
+- (NSDictionary<NSNumber *, NSNumber *> *)showIDsByEpisodeIDForShows:(NSArray<VLCMediaLibraryShow *> *)shows
 {
-    [self invalidateCacheUpdateForGetter:@selector(cachedListOfShows)];
-    dispatch_barrier_async(_mediaItemCacheModificationQueue, ^{
-        self->_cachedListOfShows = [cachedListOfShows copy];
+    NSMutableDictionary<NSNumber *, NSNumber *> * const showIDs = NSMutableDictionary.dictionary;
+    for (VLCMediaLibraryShow * const show in shows) {
+        if (show.episodeCount == 0) {
+            continue;
+        }
+        vlc_ml_media_list_t * const episodes = vlc_ml_list_show_episodes(_p_mediaLibrary, NULL, show.libraryID);
+        if (episodes == NULL) {
+            return nil;
+        }
+        for (size_t index = 0; index < episodes->i_nb_items; index++) {
+            showIDs[@(episodes->p_items[index].i_id)] = @(show.libraryID);
+        }
+        vlc_ml_media_list_release(episodes);
+    }
+    return showIDs;
+}
+
+- (void)replaceCachedShows:(NSArray<VLCMediaLibraryShow *> *)shows
+        showIDsByEpisodeID:(NSDictionary<NSNumber *, NSNumber *> *)showIDsByEpisodeID
+{
+    _cachedListOfShows = shows.copy;
+    _showIDsByEpisodeID = showIDsByEpisodeID.mutableCopy;
+    [_showRefreshTokens removeAllObjects];
+}
+
+- (void)setCachedListOfShows:(NSArray<VLCMediaLibraryShow *> *)cachedListOfShows
+{
+    NSArray<VLCMediaLibraryShow *> * const shows = cachedListOfShows.copy;
+    if (shows == nil) {
+        [self invalidateCacheUpdateForGetter:@selector(cachedListOfShows)];
+        dispatch_barrier_async(_mediaItemCacheModificationQueue, ^{
+            [self replaceCachedShows:nil showIDsByEpisodeID:nil];
+        });
+        return;
+    }
+    const NSUInteger generation = self.cacheGeneration;
+    NSObject * const token = [self beginCacheUpdateForGetter:@selector(cachedListOfShows)];
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
+        NSDictionary<NSNumber *, NSNumber *> * const showIDs = [self showIDsByEpisodeIDForShows:shows];
+        if (showIDs == nil) {
+            return;
+        }
+        [self performCacheWriteForGeneration:generation
+                                       token:token
+                                      getter:@selector(cachedListOfShows)
+                                     onQueue:self->_mediaItemCacheModificationQueue
+                                       block:^{
+            [self replaceCachedShows:shows showIDsByEpisodeID:showIDs];
+        }];
     });
 }
 
@@ -1203,12 +1254,16 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
             }
         }
         vlc_ml_show_list_release(p_show_list);
+        NSDictionary<NSNumber *, NSNumber *> * const showIDs = [self showIDsByEpisodeIDForShows:mutableArray];
+        if (showIDs == nil) {
+            return;
+        }
         [self performCacheWriteForGeneration:generation
                                        token:token
                                       getter:@selector(cachedListOfShows)
                                      onQueue:self->_mediaItemCacheModificationQueue
                                        block:^{
-            self->_cachedListOfShows = mutableArray.copy;
+            [self replaceCachedShows:mutableArray showIDsByEpisodeID:showIDs];
             [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
                 [self.changeDelegate notifyChange:VLCLibraryModelListOfShowsReset withObject:self];
             }];
@@ -1216,6 +1271,85 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
     });
 }
 
+- (void)refreshCachedShowForEpisodeID:(int64_t)episodeID
+{
+    NSNumber * const showID = _showIDsByEpisodeID[@(episodeID)];
+    if (showID == nil) {
+        return;
+    }
+    if ([self invalidateCacheUpdateForGetter:@selector(cachedListOfShows)]) {
+        // A parent-only lookup would lose other shows in the pending full list.
+        [self resetCachedListOfShows];
+        return;
+    }
+    const BOOL refreshPending = _showRefreshTokens[showID] != nil;
+    _showRefreshTokens[showID] = NSObject.new;
+    if (!refreshPending) {
+        [self refreshCachedShowForLibraryID:showID.longLongValue];
+    }
+}
+
+- (void)refreshCachedShowForLibraryID:(int64_t)libraryID
+{
+    const NSUInteger generation = self.cacheGeneration;
+    NSNumber * const showID = @(libraryID);
+    NSObject * const token = _showRefreshTokens[showID];
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
+        vlc_ml_show_t * const p_show = vlc_ml_get_show(self->_p_mediaLibrary, libraryID);
+        VLCMediaLibraryShow * const updatedShow =
+            p_show == NULL ? nil : [[VLCMediaLibraryShow alloc] initWithShow:p_show];
+        if (p_show != NULL) {
+            vlc_ml_show_release(p_show);
+        }
+        [self updateCachedShow:updatedShow
+                     libraryID:libraryID
+                    generation:generation
+                         token:token];
+    });
+}
+
+- (void)updateCachedShow:(VLCMediaLibraryShow *)updatedShow
+               libraryID:(int64_t)libraryID
+              generation:(NSUInteger)generation
+                   token:(NSObject *)token
+{
+    NSNumber * const showID = @(libraryID);
+    dispatch_barrier_async(_mediaItemCacheModificationQueue, ^{
+        NSObject * const currentToken = self->_showRefreshTokens[showID];
+        if (generation != self.cacheGeneration || currentToken == nil) {
+            return;
+        }
+        if (currentToken != token) {
+            // Coalesce changes that arrived during this lookup
+            [self refreshCachedShowForLibraryID:libraryID];
+            return;
+        }
+        [self->_showRefreshTokens removeObjectForKey:showID];
+        const NSUInteger index = [self->_cachedListOfShows indexOfObjectPassingTest:^BOOL(VLCMediaLibraryShow * const show, NSUInteger __unused idx, BOOL * __unused stop) {
+            return show.libraryID == libraryID;
+        }];
+        if (self->_cachedListOfShows == nil || index == NSNotFound) {
+            return;
+        }
+        NSMutableArray<VLCMediaLibraryShow *> * const shows = self->_cachedListOfShows.mutableCopy;
+        const BOOL deleted = updatedShow == nil || updatedShow.episodeCount == 0;
+        if (deleted) {
+            [shows removeObjectAtIndex:index];
+            NSSet<NSNumber *> * const episodeIDs = [self->_showIDsByEpisodeID keysOfEntriesPassingTest:^BOOL(NSNumber * __unused key, NSNumber * const parentID, BOOL * __unused stop) {
+                return [parentID isEqualToNumber:showID];
+            }];
+            [self->_showIDsByEpisodeID removeObjectsForKeys:episodeIDs.allObjects];
+        } else {
+            [shows replaceObjectAtIndex:index withObject:updatedShow];
+        }
+        self->_cachedListOfShows = shows.copy;
+        [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
+            [self.changeDelegate notifyChange:deleted ? VLCLibraryModelShowDeleted : VLCLibraryModelShowUpdated
+                                   withObject:deleted ? showID : updatedShow];
+        }];
+    });
+}
+
 - (void)resetCachedListOfMovies
 {
     const NSUInteger generation = self.cacheGeneration;
@@ -1593,9 +1727,6 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
                                     NSMutableArray * const,
                                     const NSUInteger,
                                     NSMutableArray * const,
-                                    const NSUInteger,
-                                    NSMutableArray * const,
-                                    const NSUInteger,
                                     const NSUInteger
                                  ))action
 {
@@ -1611,41 +1742,21 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
 
         NSArray<VLCMediaLibraryMediaItem *> * const cachedRecents = self->_cachedRecentMedia;
         NSArray<VLCMediaLibraryMediaItem *> * const cachedVideos = self->_cachedVideoMedia;
-        NSArray<VLCMediaLibraryShow *> * const cachedShows = self->_cachedListOfShows;
 
         const NSUInteger recentsIndex = cachedRecents ? [cachedRecents indexOfObjectPassingTest:idCheckBlock] : NSNotFound;
         const NSUInteger videoIndex = cachedVideos ? [cachedVideos indexOfObjectPassingTest:idCheckBlock] : NSNotFound;
 
         if (videoIndex != NSNotFound || recentsIndex != NSNotFound) {
-            // Search shows for a matching episode.
-            NSInteger showIndex = NSNotFound;
-            NSInteger episodeIndex = NSNotFound;
-            NSUInteger currentShowIndex = 0;
-            if (cachedShows) {
-                for (VLCMediaLibraryShow * const show in cachedShows) {
-                    episodeIndex = [show.episodes indexOfObjectPassingTest:idCheckBlock];
-                    if (episodeIndex != NSNotFound) {
-                        showIndex = currentShowIndex;
-                        break;
-                    }
-                    currentShowIndex++;
-                }
-            }
-
             // Create mutable copies for modification
             NSMutableArray<VLCMediaLibraryMediaItem *> * const recentsMutable = cachedRecents.mutableCopy;
             NSMutableArray<VLCMediaLibraryMediaItem *> * const videoMutable = cachedVideos.mutableCopy;
-            NSMutableArray<VLCMediaLibraryShow *> * const showsMutable =
-                showIndex == NSNotFound ? nil : cachedShows.mutableCopy;
 
-            SEL const getters[] = { @selector(cachedVideoMedia), @selector(cachedRecentMedia), @selector(cachedListOfShows) };
-            [self performCacheMutationForGetters:getters count:showsMutable != nil ? 3 : 2 block:^{
-                action(videoMutable, videoIndex, recentsMutable, recentsIndex, showsMutable, showIndex, episodeIndex);
+            SEL const getters[] = { @selector(cachedVideoMedia), @selector(cachedRecentMedia) };
+            [self performCacheMutationForGetters:getters count:2 block:^{
+                action(videoMutable, videoIndex, recentsMutable, recentsIndex);
                 // Item handlers adjust title counts individually inside this barrier.
                 self->_cachedVideoMedia = videoMutable.copy;
                 self->_cachedRecentMedia = recentsMutable.copy;
-                if (showsMutable)
-                    self->_cachedListOfShows = showsMutable.copy;
             }];
             return;
         }
@@ -1664,7 +1775,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
 
             SEL const getters[] = { @selector(cachedAudioMedia), @selector(cachedRecentAudioMedia) };
             [self performCacheMutationForGetters:getters count:2 block:^{
-                action(audioMutable, audioIndex, recentAudiosMutable, recentAudiosIndex, nil, NSNotFound, NSNotFound);
+                action(audioMutable, audioIndex, recentAudiosMutable, recentAudiosIndex);
                 self->_cachedAudioMedia = audioMutable.copy;
                 self->_cachedRecentAudioMedia = recentAudiosMutable.copy;
             }];
@@ -1672,7 +1783,7 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
         }
 
         // Not found in any cache
-        action(nil, NSNotFound, nil, NSNotFound, nil, NSNotFound, NSNotFound);
+        action(nil, NSNotFound, nil, NSNotFound);
     });
 }
 
@@ -1775,11 +1886,9 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
         NSMutableArray * const cachedMediaArray,
         const NSUInteger cachedMediaIndex,
         NSMutableArray * const recentMediaArray,
-        const NSUInteger recentMediaIndex,
-        NSMutableArray * const showsArray,
-        const NSUInteger showIndex,
-        const NSUInteger __unused showEpisodeIndex
+        const NSUInteger recentMediaIndex
     ) {
+        [self refreshCachedShowForEpisodeID:itemId];
         id<VLCMediaLibraryItemProtocol> const previousItem =
             cachedMediaIndex == NSNotFound ? nil : cachedMediaArray[cachedMediaIndex];
         const BOOL audio = mediaItem.mediaType == VLC_ML_MEDIA_TYPE_AUDIO;
@@ -1836,16 +1945,6 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
             }];
         }
 
-        if (showsArray != nil && showIndex != NSNotFound) {
-            // An episode has changed. Refresh the whole show.
-            VLCMediaLibraryShow * const staleShow = showsArray[showIndex];
-            VLCMediaLibraryShow * const updatedShow = [VLCMediaLibraryShow showWithLibraryId:staleShow.libraryID];
-            [showsArray replaceObjectAtIndex:showIndex withObject:updatedShow];
-            [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
-                [self.changeDelegate notifyChange:VLCLibraryModelShowUpdated withObject:updatedShow];
-            }];
-        }
-
         if (reloadMain) {
             return;
         }
@@ -1877,11 +1976,10 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
         NSMutableArray * const cachedMediaArray,
         const NSUInteger cachedMediaIndex,
         NSMutableArray * const recentMediaArray,
-        const NSUInteger recentMediaIndex,
-        NSMutableArray * const showsArray,
-        const NSUInteger showIndex,
-        const NSUInteger __unused showEpisodeIndex
+        const NSUInteger recentMediaIndex
     ) {
+        [self refreshCachedShowForEpisodeID:itemId];
+        [self->_showIDsByEpisodeID removeObjectForKey:@(itemId)];
 
         // A deletion also supersedes any snapshot fetched before it. Preserve
         // pending membership/order changes by querying the latest state again.
@@ -1922,26 +2020,6 @@ static void libraryCallback(void *p_data, const vlc_ml_event_t *p_event)
             }];
         }
 
-        if (showsArray != nil && showIndex != NSNotFound) {
-            // An episode has changed. Refresh the whole show.
-            VLCMediaLibraryShow * const staleShow = showsArray[showIndex];
-            VLCMediaLibraryShow * const updatedShow =
-                [VLCMediaLibraryShow showWithLibraryId:staleShow.libraryID];
-
-            if (updatedShow == nil || updatedShow.episodeCount == 0) {
-                [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
-                    [self.changeDelegate notifyChange:VLCLibraryModelShowDeleted
-                                           withObject:@(staleShow.libraryID)];
-                }];
-            } else {
-                [showsArray replaceObjectAtIndex:showIndex withObject:updatedShow];
-                [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
-                    [self.changeDelegate notifyChange:VLCLibraryModelShowUpdated
-                                           withObject:updatedShow];
-                }];
-            }
-        }
-
         [self performAfterCacheWritesOnQueue:self->_mediaItemCacheModificationQueue block:^{
             switch (mediaItem.mediaType) {
                 case VLC_ML_MEDIA_TYPE_VIDEO:



View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/cb254da470a1e85b33b10e3a63f2f8385a27bca2

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/cb254da470a1e85b33b10e3a63f2f8385a27bca2
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