Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/VLC/modules/gui/macosx/library/   (VLC Media Plyer ©)  Datei vom 23.9.2026 mit Größe 33 kB image not shown  

Quelle  VLCLibraryImageCache.m  Sprache: unbekannt

 
Spracherkennung für: .m vermutete Sprache: Masm {Masm[60] ABAP[43] Cobol[36]} [Methode: maximale Elemente, drei Dimensionen]

/*****************************************************************************
* VLCLibraryImageCache.m: MacOS X interface module
*****************************************************************************
* Copyright (C) 2020-2026 VLC authors and VideoLAN
*
* Authors: Felix Paul Kühne <fkuehne # videolan -dot- org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/

#import "VLCLibraryImageCache.h"

#import "extensions/NSImage+VLCAdditions.h"

#import "library/VLCInputItem.h"
#import "library/VLCLibraryDataTypes.h"
#import "library/VLCLibraryModel.h"
#import "library/VLCThumbnailRequest.h"

#import "main/VLCMain.h"

#import "playqueue/VLCPlayQueueItem.h"

#import <ImageIO/ImageIO.h>

#import <vlc_configuration.h>
#import <vlc_hash.h>
#import <vlc_preparser.h>
#import <vlc_strings.h>

static const NSTimeInterval kVLCRemoteArtworkRequestTimeout = 15;
static const NSTimeInterval kVLCRemoteArtworkResourceTimeout = 30;
static const NSInteger kVLCRemoteArtworkMaximumConnectionsPerHost = 4;

NSUInteger kVLCMaximumLibraryImageCacheSize = 500;
/* 256 MB cost limit based on estimated pixel data size per image */
static const NSUInteger kVLCLibraryImageCacheCostLimit = 256 * 1024 * 1024;
static const NSTimeInterval kVLCThumbnailCacheMaximumAge = 30 * 24 * 60 * 60;
uint32_t kVLCDesiredThumbnailWidth = 512;
uint32_t kVLCDesiredThumbnailHeight = 512;
float kVLCDefaultThumbnailPosition = .15;
const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;

@interface VLCLibraryImageCache()
{
    NSCache *_imageCache;
    NSImage *_noArtImage;
    vlc_medialibrary_t *_p_libraryInstance;
    NSString *_thumbnailCacheDirectory;
    NSMutableDictionary<NSString *, VLCThumbnailRequest *> *_pendingThumbnailRequests;
    NSMutableDictionary<NSString *, NSMutableArray<void (^)(const NSImage *)> *> *_pendingRemoteArtworkRequests;
    NSURLSession *_remoteArtworkSession;
}

- (void)thumbnailRequest:(VLCThumbnailRequest *)request
     didFinishWithStatus:(int)status
                 results:(const bool *)results
             resultCount:(size_t)resultCount;

@end

static void vlcThumbnailerToFilesOnEnded(vlc_preparser_req *req,
                                         int status,
                                         const bool *results,
                                         size_t resultCount,
                                         void *data)
{
    @autoreleasepool {
        VLCThumbnailRequest * const request = (__bridge VLCThumbnailRequest *)data;
        [request.imageCache thumbnailRequest:request
                         didFinishWithStatus:status
                                     results:results
                                 resultCount:resultCount];
    }
    vlc_preparser_req_Release(req);
}

static NSString *thumbnailHashForString(NSString *string)
{
    NSData * const data = [string dataUsingEncoding:NSUTF8StringEncoding];
    char hash[VLC_HASH_MD5_DIGEST_HEX_SIZE];
    vlc_hash_md5_t md5;
    vlc_hash_md5_Init(&md5);
    vlc_hash_md5_Update(&md5, data.bytes, data.length);
    vlc_hash_FinishHex(&md5, hash);
    return [NSString stringWithUTF8String:hash];
}

@implementation VLCLibraryImageCache

+ (NSImage *)downsampledImageFromSource:(CGImageSourceRef)imageSource
                           maxPixelSize:(uint32_t)maxPixelSize
{
    NSDictionary * const downsampleOptions = @{
        (NSString *)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
        (NSString *)kCGImageSourceCreateThumbnailWithTransform : @YES,
        (NSString *)kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize),
        (NSString *)kCGImageSourceShouldCacheImmediately : @YES,
    };

    CGImageRef const downsampledImage =
        CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)downsampleOptions);
    if (!downsampledImage) {
        return nil;
    }

    NSImage * const image = [[NSImage alloc] initWithCGImage:downsampledImage
                                                        size:NSZeroSize];
    CGImageRelease(downsampledImage);
    return image;
}

+ (NSImage *)downsampledImageFromURL:(NSURL *)url maxPixelSize:(uint32_t)maxPixelSize
{
    CGImageSourceRef const imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
    if (!imageSource) {
        return nil;
    }
    NSImage * const image = [self downsampledImageFromSource:imageSource maxPixelSize:maxPixelSize];
    CFRelease(imageSource);
    return image;
}

+ (BOOL)artworkURLRequiresRemoteFetch:(NSURL *)url
{
    return [url.scheme isEqualToString:@"http"] ||
           [url.scheme isEqualToString:@"https"];
}

+ (BOOL)remoteArtworkResponseCanContainImage:(NSURLResponse *)response
{
    if ([response isKindOfClass:NSHTTPURLResponse.class]) {
        NSHTTPURLResponse * const httpResponse = (NSHTTPURLResponse *)response;
        if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) {
            return NO;
        }
    }

    NSString * const MIMEType = response.MIMEType.lowercaseString;
    if (MIMEType.length > 0 &&
        ![MIMEType hasPrefix:@"image/"] &&
        ![MIMEType isEqualToString:@"application/octet-stream"]) {
        return NO;
    }

    return YES;
}

+ (NSUInteger)costForImage:(NSImage *)image
{
    NSBitmapImageRep *bitmapRep = nil;
    for (NSImageRep *rep in image.representations) {
        if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
            bitmapRep = (NSBitmapImageRep *)rep;
            break;
        }
    }
    if (bitmapRep) {
        return bitmapRep.pixelsWide * bitmapRep.pixelsHigh * bitmapRep.bitsPerPixel / 8;
    }
    return (NSUInteger)(image.size.width * image.size.height * 4);
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _imageCache = [[NSCache alloc] init];
        _imageCache.countLimit = kVLCMaximumLibraryImageCacheSize;
        _imageCache.totalCostLimit = kVLCLibraryImageCacheCostLimit;
        _noArtImage = NSImage.VLCNoArtImage;
        _pendingThumbnailRequests = NSMutableDictionary.dictionary;
        _pendingRemoteArtworkRequests = NSMutableDictionary.dictionary;

        NSURLSessionConfiguration * const remoteArtworkSessionConfiguration =
            NSURLSessionConfiguration.ephemeralSessionConfiguration;
        remoteArtworkSessionConfiguration.timeoutIntervalForRequest =
            kVLCRemoteArtworkRequestTimeout;
        remoteArtworkSessionConfiguration.timeoutIntervalForResource =
            kVLCRemoteArtworkResourceTimeout;
        remoteArtworkSessionConfiguration.requestCachePolicy =
            NSURLRequestReloadIgnoringLocalCacheData;
        remoteArtworkSessionConfiguration.HTTPMaximumConnectionsPerHost =
            kVLCRemoteArtworkMaximumConnectionsPerHost;
        _remoteArtworkSession =
            [NSURLSession sessionWithConfiguration:remoteArtworkSessionConfiguration];

        char * const cacheDirectory = config_GetUserDir(VLC_CACHE_DIR);
        if (cacheDirectory != NULL) {
            NSString * const thumbnailCacheDirectory =
                [NSString stringWithFormat:@"%s/macosx-thumbnails", cacheDirectory];
            free(cacheDirectory);

            NSError *error = nil;
            if ([NSFileManager.defaultManager createDirectoryAtPath:thumbnailCacheDirectory
                                        withIntermediateDirectories:YES
                                                         attributes:@{NSFilePosixPermissions: @0700}
                                                              error:&error]) {
                _thumbnailCacheDirectory = [thumbnailCacheDirectory copy];
                dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
                    [self pruneStoredThumbnailCache];
                });
            } else {
                NSLog(@"Failed to create thumbnail cache directory %@: %@",
                      thumbnailCacheDirectory, error);
            }
        }

        NSNotificationCenter * const notificationCenter = [NSNotificationCenter defaultCenter];
        [notificationCenter addObserver:self
                               selector:@selector(mediaItemThumbnailGenerated:)
                                   name:VLCLibraryModelMediaItemThumbnailGenerated
                                 object:nil];
        [notificationCenter addObserver:self
                               selector:@selector(mediaItemUpdated:)
                                   name:VLCLibraryModelAudioMediaItemUpdated
                                 object:nil];
        [notificationCenter addObserver:self
                               selector:@selector(mediaItemUpdated:)
                                   name:VLCLibraryModelVideoMediaItemUpdated
                                 object:nil];
    }
    return self;
}

- (void)dealloc
{
    [_remoteArtworkSession invalidateAndCancel];
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)pruneStoredThumbnailCache
{
    if (_thumbnailCacheDirectory == nil) {
        return;
    }

    @synchronized (self) {
        NSArray<NSString *> * const cachedFiles =
            [NSFileManager.defaultManager contentsOfDirectoryAtPath:_thumbnailCacheDirectory
                                                              error:NULL];
        NSDate * const now = NSDate.date;

        for (NSString * const cachedFile in cachedFiles) {
            if (![cachedFile.pathExtension isEqualToString:@"jpg"]) {
                continue;
            }

            NSString * const cachedPath =
                [_thumbnailCacheDirectory stringByAppendingPathComponent:cachedFile];
            NSDictionary * const attributes =
                [NSFileManager.defaultManager attributesOfItemAtPath:cachedPath error:NULL];
            NSDate * const modificationDate = attributes[NSFileModificationDate];
            if (modificationDate != nil &&
                [now timeIntervalSinceDate:modificationDate] > kVLCThumbnailCacheMaximumAge) {
                [NSFileManager.defaultManager removeItemAtPath:cachedPath error:NULL];
            }
        }
    }
}

- (nullable NSString *)thumbnailPathForInputItem:(VLCInputItem *)inputItem
                                        cacheKey:(NSString *)cacheKey
{
    if (_thumbnailCacheDirectory == nil || inputItem.path.length == 0 || cacheKey.length == 0) {
        return nil;
    }

    NSString * const sourceKey = inputItem.path;
    NSString * const sourceHash = thumbnailHashForString(sourceKey);
    NSDictionary * const sourceAttributes =
        [NSFileManager.defaultManager attributesOfItemAtPath:inputItem.path error:NULL];
    NSDate * const modificationDate = sourceAttributes[NSFileModificationDate];
    NSNumber * const sourceSize = sourceAttributes[NSFileSize];
    NSString * const identifier = [NSString stringWithFormat:@"%@-%@-%llu-%.6f-%ux%u-%.6f",
                                   sourceKey,
                                   cacheKey,
                                   sourceSize.unsignedLongLongValue,
                                   modificationDate.timeIntervalSince1970,
                                   kVLCDesiredThumbnailWidth,
                                   kVLCDesiredThumbnailHeight,
                                   kVLCDefaultThumbnailPosition];
    NSString * const thumbnailHash = thumbnailHashForString(identifier);

    return [_thumbnailCacheDirectory stringByAppendingPathComponent:
            [NSString stringWithFormat:@"%@-%@.jpg", sourceHash, thumbnailHash]];
}

- (nullable NSImage *)thumbnailFromCacheForInputItem:(VLCInputItem *)inputItem
                                            cacheKey:(NSString *)cacheKey
{
    NSString * const thumbnailPath = [self thumbnailPathForInputItem:inputItem
                                                            cacheKey:cacheKey];
    if (thumbnailPath == nil) {
        return nil;
    }

    [self removeStaleThumbnailFilesForInputItem:inputItem
                                       cacheKey:cacheKey
                                    keepingPath:thumbnailPath];
    NSImage * const image = [[NSImage alloc] initWithContentsOfFile:thumbnailPath];
    if (image == nil && [NSFileManager.defaultManager fileExistsAtPath:thumbnailPath]) {
        [NSFileManager.defaultManager removeItemAtPath:thumbnailPath error:NULL];
    }
    return image;
}

- (void)removeStaleThumbnailFilesForInputItem:(VLCInputItem *)inputItem
                                     cacheKey:(NSString *)cacheKey
                                  keepingPath:(NSString *)keepingPath
{
    if (_thumbnailCacheDirectory == nil || inputItem.path.length == 0 || cacheKey.length == 0) {
        return;
    }

    NSString * const sourceKey = inputItem.path;
    NSString * const sourcePrefix = [thumbnailHashForString(sourceKey) stringByAppendingString:@"-"];
    NSArray<NSString *> * const cachedFiles =
        [NSFileManager.defaultManager contentsOfDirectoryAtPath:_thumbnailCacheDirectory error:NULL];
    for (NSString * const cachedFile in cachedFiles) {
        if (![cachedFile hasPrefix:sourcePrefix] || ![cachedFile.pathExtension isEqualToString:@"jpg"]) {
            continue;
        }

        NSString * const cachedPath = [_thumbnailCacheDirectory stringByAppendingPathComponent:cachedFile];
        if (![cachedPath isEqualToString:keepingPath]) {
            [NSFileManager.defaultManager removeItemAtPath:cachedPath error:NULL];
        }
    }
}

+ (instancetype)sharedImageCache
{
    static dispatch_once_t onceToken;
    static VLCLibraryImageCache *sharedImageCache;
    dispatch_once(&onceToken, ^{
        sharedImageCache = [[VLCLibraryImageCache alloc] init];
    });
    return sharedImageCache;
}

- (void)mediaItemThumbnailGenerated:(NSNotification *)aNotification
{
    VLCMediaLibraryMediaItem * const mediaItem = aNotification.object;
    NSString * const artworkMRL = mediaItem.smallArtworkMRL;
    if (mediaItem == nil || artworkMRL == nil) {
        return;
    }
    [_imageCache removeObjectForKey:artworkMRL];
}

- (void)mediaItemUpdated:(NSNotification *)aNotification
{
    VLCMediaLibraryMediaItem * const mediaItem = aNotification.object;
    NSString * const artworkMRL = mediaItem.smallArtworkMRL;
    if (mediaItem == nil || artworkMRL == nil) {
        return;
    }
    [_imageCache removeObjectForKey:artworkMRL];
}

- (void)imageForLibraryItem:(id<VLCMediaLibraryItemProtocol>)libraryItem
             withCompletion:(void(^)(const NSImage *))completionHandler
{
    NSString * const artworkMRL = libraryItem.smallArtworkMRL;
    if (artworkMRL) {
        NSImage * const cachedImage = [_imageCache objectForKey:artworkMRL];
        if (cachedImage) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionHandler(cachedImage);
            });
            return;
        }
    }

    if (libraryItem.smallArtworkGenerated && artworkMRL) {
        NSURL * const artworkURL = [NSURL URLWithString:artworkMRL];
        dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
            NSImage * const image =
                [VLCLibraryImageCache downsampledImageFromURL:artworkURL
                                                 maxPixelSize:kVLCDesiredThumbnailWidth];
            if (image) {
                const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
                [self->_imageCache setObject:image forKey:artworkMRL cost:cost];
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                completionHandler(image ?: self->_noArtImage);
            });
        });
    } else if ([libraryItem isKindOfClass:[VLCMediaLibraryMediaItem class]]) {
        VLCMediaLibraryMediaItem * const mediaItem = (VLCMediaLibraryMediaItem *)libraryItem;
        if (mediaItem.mediaType != VLC_ML_MEDIA_TYPE_AUDIO) {
            [self generateThumbnailForMediaItem:mediaItem.libraryID];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            completionHandler(self->_noArtImage);
        });
    } else {
        dispatch_async(dispatch_get_main_queue(), ^{
            completionHandler(self->_noArtImage);
        });
    }
}

- (void)generateThumbnailForMediaItem:(int64_t)mediaID
{
    if (!_p_libraryInstance) {
        _p_libraryInstance = vlc_ml_instance_get(getIntf());
    }
    vlc_ml_media_generate_thumbnail(_p_libraryInstance,
                                    mediaID,
                                    VLC_ML_THUMBNAIL_SMALL,
                                    kVLCDesiredThumbnailWidth,
                                    kVLCDesiredThumbnailHeight,
                                    kVLCDefaultThumbnailPosition);
}

+ (void)thumbnailForInputItem:(VLCInputItem *)inputItem 
               withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
{
    [VLCLibraryImageCache.sharedImageCache imageForInputItem:inputItem withCompletion:completionHandler];
}

- (void)imageForCountryCode:(NSString *)countryCode
             withCompletion:(void(^)(const NSImage * _Nullable))completionHandler
{
    NSString * const normalizedCountryCode = countryCode.uppercaseString;
    NSString * const cacheKey = [@"flag://" stringByAppendingString:normalizedCountryCode];
    NSImage * const cachedImage = [_imageCache objectForKey:cacheKey];
    if (cachedImage) {
        dispatch_async(dispatch_get_main_queue(), ^{
            completionHandler(cachedImage);
        });
        return;
    }

    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        const NSSize imageSize =
            NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
        NSImage * const image = [NSImage flagImageForCountryCode:normalizedCountryCode
                                                           size:imageSize];
        if (image) {
            const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
            [self->_imageCache setObject:image forKey:cacheKey cost:cost];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            completionHandler(image);
        });
    });
}

- (void)imageForInputItem:(VLCInputItem *)inputItem
           withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
{
    NSURL * const artworkURL = inputItem.artworkURL;
    NSString * const cacheKey = [VLCLibraryImageCache artworkURLRequiresRemoteFetch:artworkURL]
        ? artworkURL.absoluteString
        : inputItem.MRL;

    NSImage * const cachedImage = [_imageCache objectForKey:cacheKey];
    if (cachedImage) {
        dispatch_async(dispatch_get_main_queue(), ^{
            completionHandler(cachedImage);
        });
        return;
    }
    [self generateImageForInputItem:inputItem withCompletion:completionHandler];
}

- (void)fetchRemoteArtwork:(NSURL *)url
               forCacheKey:(NSString *)cacheKey
            withCompletion:(void(^)(const NSImage *))completionHandler
{
    @synchronized (self) {
        NSMutableArray<void (^)(const NSImage *)> * const pendingCompletions =
            _pendingRemoteArtworkRequests[cacheKey];
        if (pendingCompletions != nil) {
            [pendingCompletions addObject:[completionHandler copy]];
            return;
        }

        _pendingRemoteArtworkRequests[cacheKey] =
            [NSMutableArray arrayWithObject:[completionHandler copy]];
    }

    [[_remoteArtworkSession dataTaskWithURL:url
                          completionHandler:^(NSData *data,
                                              NSURLResponse *response,
                                              NSError *error) {
        NSImage *image = nil;

        if (!error && [VLCLibraryImageCache remoteArtworkResponseCanContainImage:response] && data) {
            CGImageSourceRef const source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
            if (source) {
                image = [VLCLibraryImageCache downsampledImageFromSource:source
                                                            maxPixelSize:kVLCDesiredThumbnailWidth];
                CFRelease(source);
            }
        }
        if (image) {
            const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
            [self->_imageCache setObject:image forKey:cacheKey cost:cost];
        }

        NSArray<void (^)(const NSImage *)> *completionHandlers;
        @synchronized (self) {
            completionHandlers = [self->_pendingRemoteArtworkRequests[cacheKey] copy];
            [self->_pendingRemoteArtworkRequests removeObjectForKey:cacheKey];
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            const NSImage * const artwork = image ?: self->_noArtImage;
            for (void (^handler)(const NSImage *) in completionHandlers) {
                handler(artwork);
            }
        });
    }] resume];
}

- (void)generateArtworkForInputItem:(VLCInputItem *)inputItem
                     withCompletion:(void(^)(const NSImage *))completionHandler
{
    NSURL * const artworkURL = inputItem.artworkURL;
    const NSSize imageSize = NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
    NSString * const memoryCacheKey = inputItem.MRL;

    if ([VLCLibraryImageCache artworkURLRequiresRemoteFetch:artworkURL]) {
        [self fetchRemoteArtwork:artworkURL
                     forCacheKey:artworkURL.absoluteString
                  withCompletion:completionHandler];
        return;
    }

    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        NSImage * const image = artworkURL
            ? [VLCLibraryImageCache downsampledImageFromURL:artworkURL
                                               maxPixelSize:kVLCDesiredThumbnailWidth]
            : nil;

        if (image) {
            const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
            [self->_imageCache setObject:image forKey:memoryCacheKey cost:cost];
            dispatch_async(dispatch_get_main_queue(), ^{
                completionHandler(image);
            });
            return;
        }

        if (inputItem.inputType == ITEM_TYPE_FILE &&
            !inputItem.isStream &&
            input_item_Playable(inputItem.path.UTF8String)) {
            NSImage * const cachedThumbnail =
                [self thumbnailFromCacheForInputItem:inputItem
                                            cacheKey:memoryCacheKey];
            if (cachedThumbnail != nil) {
                const NSUInteger cost = [VLCLibraryImageCache costForImage:cachedThumbnail];
                [self->_imageCache setObject:cachedThumbnail forKey:memoryCacheKey cost:cost];
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionHandler(cachedThumbnail);
                });
                return;
            }

            [self generateVLCThumbnailForInputItem:inputItem
                                          cacheKey:inputItem.MRL
                                        completion:^(NSImage * const thumbnail) {
                if (thumbnail != nil) {
                    const NSUInteger cost = [VLCLibraryImageCache costForImage:thumbnail];
                    [self->_imageCache setObject:thumbnail forKey:memoryCacheKey cost:cost];
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completionHandler(thumbnail);
                    });
                    return;
                }

                [inputItem thumbnailWithSize:imageSize completionHandler:^(NSImage * const quickLookThumbnail) {
                    if (quickLookThumbnail) {
                        const NSUInteger cost = [VLCLibraryImageCache costForImage:quickLookThumbnail];
                        [self->_imageCache setObject:quickLookThumbnail forKey:memoryCacheKey cost:cost];
                    } else {
                        NSLog(@"Failed to generate thumbnail for input item %@", inputItem.MRL);
                    }
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completionHandler(quickLookThumbnail ?: self->_noArtImage);
                    });
                }];
            }];
            return;
        }

        if (inputItem.isStream || inputItem.path.length == 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionHandler(self->_noArtImage);
            });
            return;
        }

        [inputItem thumbnailWithSize:imageSize completionHandler:^(NSImage * const quickLookThumbnail) {
            if (quickLookThumbnail) {
                const NSUInteger cost = [VLCLibraryImageCache costForImage:quickLookThumbnail];
                [self->_imageCache setObject:quickLookThumbnail forKey:memoryCacheKey cost:cost];
            } else {
                NSLog(@"Failed to generate thumbnail for input item %@", inputItem.MRL);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                completionHandler(quickLookThumbnail ?: self->_noArtImage);
            });
        }];
    });
}

- (void)generateImageForInputItem:(VLCInputItem *)inputItem
                   withCompletion:(void(^)(const NSImage *))completionHandler
{
    NSString * const radioCountryCode = inputItem.radioCountryCodeForFlagArtwork;
    if (!radioCountryCode) {
        [self generateArtworkForInputItem:inputItem withCompletion:completionHandler];
        return;
    }

    [self imageForCountryCode:radioCountryCode
               withCompletion:^(const NSImage * const flagImage) {
        if (flagImage) {
            completionHandler(flagImage);
            return;
        }
        [self generateArtworkForInputItem:inputItem withCompletion:completionHandler];
    }];
}

- (void)generateVLCThumbnailForInputItem:(VLCInputItem *)inputItem
                                cacheKey:(NSString *)cacheKey
                              completion:(VLCThumbnailCompletion)completion
{
    VLCThumbnailRequest *request;
    @synchronized (self) {
        request = _pendingThumbnailRequests[cacheKey];
        if (request != nil) {
            [request.completionHandlers addObject:[completion copy]];
            return;
        }

        request = [[VLCThumbnailRequest alloc] init];
        request.imageCache = self;
        request.cacheKey = cacheKey;
        request.thumbnailPath = [self thumbnailPathForInputItem:inputItem
                                                       cacheKey:cacheKey];
        request.completionHandlers = [NSMutableArray arrayWithObject:[completion copy]];
        _pendingThumbnailRequests[cacheKey] = request;
    }

    vlc_preparser_t * const thumbnailer = getThumbnailer();
    if (thumbnailer == NULL || request.thumbnailPath == nil) {
        [self thumbnailRequest:request
           didFinishWithStatus:VLC_EGENERIC
                       results:NULL
                   resultCount:0];
        return;
    }

    struct vlc_thumbnailer_arg thumbnailerArgument = {
        .seek = {
            .type = VLC_THUMBNAILER_SEEK_POS,
            .pos = kVLCDefaultThumbnailPosition,
            .speed = VLC_THUMBNAILER_SEEK_FAST,
        },
        .hw_dec = false,
    };
    const struct vlc_thumbnailer_output output = {
        .format = VLC_THUMBNAILER_FORMAT_JPEG,
        .width = kVLCDesiredThumbnailWidth,
        .height = kVLCDesiredThumbnailHeight,
        .crop = true,
        .file_path = request.thumbnailPath.fileSystemRepresentation,
        .creat_mode = 0600,
    };
    static const struct vlc_thumbnailer_to_files_cbs callbacks = {
        .on_ended = vlcThumbnailerToFilesOnEnded,
    };

    vlc_preparser_req * const requestHandle = vlc_preparser_req_NewThumbnailToFiles(
        thumbnailer,
        inputItem.vlcInputItem,
        &thumbnailerArgument,
        &output,
        1,
        &callbacks,
        (__bridge void *)request);
    if (requestHandle != NULL) {
        if (vlc_preparser_Submit(thumbnailer, requestHandle) == VLC_SUCCESS) {
            return;
        }
        vlc_preparser_req_Release(requestHandle);
    }

    [self thumbnailRequest:request
       didFinishWithStatus:VLC_EGENERIC
                   results:NULL
               resultCount:0];
}

- (void)thumbnailRequest:(VLCThumbnailRequest *)request
     didFinishWithStatus:(int)status
                 results:(const bool *)results
             resultCount:(size_t)resultCount
{
    NSArray<VLCThumbnailCompletion> *completionHandlers;
    @synchronized (self) {
        [_pendingThumbnailRequests removeObjectForKey:request.cacheKey];
        completionHandlers = [request.completionHandlers copy];
    }

    if (status == -EINTR) {
        for (VLCThumbnailCompletion completion in completionHandlers) {
            completion(nil);
        }
        return;
    }

    NSImage *image = nil;
    if (status == VLC_SUCCESS && resultCount == 1 && results != NULL && results[0]) {
        image = [[NSImage alloc] initWithContentsOfFile:request.thumbnailPath];
    }

    if (image == nil && request.thumbnailPath != nil) {
        [NSFileManager.defaultManager removeItemAtPath:request.thumbnailPath error:NULL];
    }

    for (VLCThumbnailCompletion completion in completionHandlers) {
        completion(image);
    }
}

+ (void)thumbnailForPlayQueueItem:(VLCPlayQueueItem *)playQueueItem
                   withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
{
    VLCMediaLibraryMediaItem * const mediaItem = playQueueItem.mediaLibraryItem;
    if (mediaItem.smallArtworkGenerated && mediaItem.smallArtworkMRL.length > 0) {
        return [VLCLibraryImageCache thumbnailForLibraryItem:mediaItem
                                              withCompletion:completionHandler];
    }

    return [VLCLibraryImageCache thumbnailForInputItem:playQueueItem.inputItem
                                        withCompletion:completionHandler];
}

+ (void)thumbnailForLibraryItem:(id<VLCMediaLibraryItemProtocol>)libraryItem
                 withCompletion:(void(^)(const NSImage *))completionHandler
{
    VLCLibraryImageCache * const cache = [VLCLibraryImageCache sharedImageCache];

    if (![libraryItem isKindOfClass:VLCMediaLibraryAlbum.class] &&
        ![libraryItem isKindOfClass:VLCMediaLibraryMediaItem.class]) {

        dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
            NSMutableSet<NSString *> * const seenMRLs = NSMutableSet.set;
            NSMutableArray<VLCMediaLibraryMediaItem *> * const uniqueItems =
                [NSMutableArray arrayWithCapacity:kVLCCompositeImageDefaultCompositedGridItemCount];

            [libraryItem enumerateMediaItemsWithBlock:^(VLCMediaLibraryMediaItem * const item, BOOL * const stop) {
                if (uniqueItems.count == (NSUInteger)kVLCCompositeImageDefaultCompositedGridItemCount) {
                    *stop = YES;
                    return;
                }
                NSString * const mrl = item.smallArtworkMRL;
                if (mrl == nil || [seenMRLs containsObject:mrl]) {
                    return;
                }
                [seenMRLs addObject:mrl];
                [uniqueItems addObject:item];
            }];

            if (uniqueItems.count == 0) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionHandler(cache->_noArtImage);
                });
                return;
            }

            NSMutableArray<NSImage *> * const itemImages =
                [NSMutableArray arrayWithCapacity:uniqueItems.count];
            dispatch_group_t const group = dispatch_group_create();

            for (VLCMediaLibraryMediaItem * const item in uniqueItems) {
                dispatch_group_enter(group);
                [cache imageForLibraryItem:item withCompletion:^(const NSImage * thumbnail) {
                    NSImage * const mutableRef = (NSImage *)thumbnail;
                    if (mutableRef && ![mutableRef isEqual:cache->_noArtImage]) {
                        @synchronized (itemImages) {
                            [itemImages addObject:mutableRef];
                        }
                    }
                    dispatch_group_leave(group);
                }];
            }

            dispatch_group_notify(group, dispatch_get_main_queue(), ^{
                if (itemImages.count == 0) {
                    completionHandler(cache->_noArtImage);
                    return;
                }

                const NSSize size = NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
                NSArray<NSValue *> * const frames =
                    [NSImage framesForCompositeImageSquareGridWithImages:itemImages
                                                                    size:size
                                                           gridItemCount:kVLCCompositeImageDefaultCompositedGridItemCount];
                NSImage * const compositeImage =
                    [NSImage compositeImageWithImages:itemImages frames:frames size:size];

                completionHandler(compositeImage);
            });
        });
    } else {
        [cache imageForLibraryItem:libraryItem withCompletion:completionHandler];
    }
}

@end

[Dauer der Verarbeitung: 0.5 Sekunden, vorverarbeitet 2026-10-24]