diff options
Diffstat (limited to 'macosx/HBQueue.m')
-rw-r--r-- | macosx/HBQueue.m | 765 |
1 files changed, 765 insertions, 0 deletions
diff --git a/macosx/HBQueue.m b/macosx/HBQueue.m new file mode 100644 index 000000000..6aee92452 --- /dev/null +++ b/macosx/HBQueue.m @@ -0,0 +1,765 @@ +/* HBQueue.m $ + + This file is part of the HandBrake source code. + Homepage: <http://handbrake.fr/>. + It may be used under the terms of the GNU General Public License. */ + +#import "HBQueue.h" +#import "NSArray+HBAdditions.h" + +NSString * const HBQueueDidAddItemNotification = @"HBQueueDidAddItemNotification"; +NSString * const HBQueueDidRemoveItemNotification = @"HBQueueDidRemoveItemNotification"; +NSString * const HBQueueDidChangeItemNotification = @"HBQueueDidChangeItemNotification"; + +NSString * const HBQueueItemNotificationIndexesKey = @"HBQueueReloadItemsNotification"; + +NSString * const HBQueueDidMoveItemNotification = @"HBQueueDidMoveItemNotification"; +NSString * const HBQueueItemNotificationSourceIndexesKey = @"HBQueueItemNotificationSourceIndexesKey"; +NSString * const HBQueueItemNotificationTargetIndexesKey = @"HBQueueItemNotificationTargetIndexesKey"; + +NSString * const HBQueueReloadItemsNotification = @"HBQueueReloadItemsNotification"; + +NSString * const HBQueueLowSpaceAlertNotification = @"HBQueueLowSpaceAlertNotification"; + +NSString * const HBQueueProgressNotification = @"HBQueueProgressNotification"; +NSString * const HBQueueProgressNotificationPercentKey = @"HBQueueProgressNotificationPercentKey"; +NSString * const HBQueueProgressNotificationInfoKey = @"HBQueueProgressNotificationInfoKey"; + +NSString * const HBQueueDidStartNotification = @"HBQueueDidStartNotification"; +NSString * const HBQueueDidCompleteNotification = @"HBQueueDidCompleteNotification"; + +NSString * const HBQueueDidCompleteItemNotification = @"HBQueueDidCompleteItemNotification"; +NSString * const HBQueueDidCompleteItemNotificationItemKey = @"HBQueueDidCompleteItemNotificationItemKey"; + +@interface HBQueue () + +@property (nonatomic) BOOL stop; + +@end + +@implementation HBQueue + +- (instancetype)initWithURL:(NSURL *)queueURL +{ + self = [super init]; + if (self) + { + NSInteger loggingLevel = [NSUserDefaults.standardUserDefaults integerForKey:@"LoggingLevel"]; + + // Init a separate instance of libhb for the queue + _core = [[HBCore alloc] initWithLogLevel:loggingLevel name:@"QueueCore"]; + _core.automaticallyPreventSleep = NO; + + _items = [[HBDistributedArray alloc] initWithURL:queueURL class:[HBQueueItem class]]; + + // Set up the observers + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadQueue) name:HBDistributedArrayChanged object:_items]; + + [self updateStats]; + } + return self; +} + +#pragma mark - Public methods + +- (void)addJob:(HBJob *)item +{ + NSParameterAssert(item); + [self addJobs:@[item]]; +} + +- (void)addJobs:(NSArray<HBJob *> *)jobs; +{ + NSParameterAssert(jobs); + + NSMutableArray<HBQueueItem *> *itemsToAdd = [NSMutableArray array]; + for (HBJob *job in jobs) + { + HBQueueItem *item = [[HBQueueItem alloc] initWithJob:job]; + [itemsToAdd addObject:item]; + } + if (itemsToAdd.count) + { + NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.items.count, itemsToAdd.count)]; + [self addQueueItems:itemsToAdd atIndexes:indexes]; + } +} + +- (BOOL)itemExistAtURL:(NSURL *)url +{ + NSParameterAssert(url); + + for (HBQueueItem *item in self.items) + { + if ((item.state == HBQueueItemStateReady || item.state == HBQueueItemStateWorking) + && [item.completeOutputURL isEqualTo:url]) + { + return YES; + } + } + return NO; +} + +- (NSUInteger)count +{ + return self.items.count; +} + +- (void)addQueueItems:(NSArray<HBQueueItem *> *)items atIndexes:(NSIndexSet *)indexes +{ + NSParameterAssert(items); + NSParameterAssert(indexes); + [self.items beginTransaction]; + + // Forward + NSUInteger currentIndex = indexes.firstIndex; + NSUInteger currentObjectIndex = 0; + while (currentIndex != NSNotFound) + { + [self.items insertObject:items[currentObjectIndex] atIndex:currentIndex]; + currentIndex = [indexes indexGreaterThanIndex:currentIndex]; + currentObjectIndex++; + } + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidAddItemNotification object:self userInfo:@{HBQueueItemNotificationIndexesKey: indexes}]; + + NSUndoManager *undo = self.undoManager; + [[undo prepareWithInvocationTarget:self] removeQueueItemsAtIndexes:indexes]; + + if (!undo.isUndoing) + { + if (items.count == 1) + { + [undo setActionName:NSLocalizedString(@"Add Job To Queue", @"Queue undo action name")]; + } + else + { + [undo setActionName:NSLocalizedString(@"Add Jobs To Queue", @"Queue undo action name")]; + } + } + + [self updateStats]; + [self.items commit]; +} + +- (void)removeQueueItemAtIndex:(NSUInteger)index +{ + [self removeQueueItemsAtIndexes:[NSIndexSet indexSetWithIndex:index]]; +} + +- (void)removeQueueItemsAtIndexes:(NSIndexSet *)indexes +{ + NSParameterAssert(indexes); + + if (indexes.count == 0) + { + return; + } + + [self.items beginTransaction]; + + NSArray<HBQueueItem *> *removeItems = [self.items objectsAtIndexes:indexes]; + + if (self.items.count > indexes.lastIndex) + { + [self.items removeObjectsAtIndexes:indexes]; + } + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidRemoveItemNotification object:self userInfo:@{HBQueueItemNotificationIndexesKey: indexes}]; + + NSUndoManager *undo = self.undoManager; + [[undo prepareWithInvocationTarget:self] addQueueItems:removeItems atIndexes:indexes]; + + if (!undo.isUndoing) + { + if (indexes.count == 1) + { + [undo setActionName:NSLocalizedString(@"Remove Job From Queue", @"Queue undo action name")]; + } + else + { + [undo setActionName:NSLocalizedString(@"Remove Jobs From Queue", @"Queue undo action name")]; + } + } + + [self updateStats]; + [self.items commit]; +} + +- (void)moveQueueItems:(NSArray<HBQueueItem *> *)items toIndex:(NSUInteger)index +{ + [self.items beginTransaction]; + + NSMutableArray<NSNumber *> *source = [NSMutableArray array]; + NSMutableArray<NSNumber *> *dest = [NSMutableArray array]; + + for (id object in items.reverseObjectEnumerator) + { + NSUInteger sourceIndex = [self.items indexOfObject:object]; + [self.items removeObjectAtIndex:sourceIndex]; + + if (sourceIndex < index) + { + index--; + } + + [self.items insertObject:object atIndex:index]; + + [source addObject:@(index)]; + [dest addObject:@(sourceIndex)]; + } + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidMoveItemNotification + object:self + userInfo:@{HBQueueItemNotificationSourceIndexesKey: dest, + HBQueueItemNotificationTargetIndexesKey: source}]; + + NSUndoManager *undo = self.undoManager; + [[undo prepareWithInvocationTarget:self] moveQueueItemsAtIndexes:source toIndexes:dest]; + + if (!undo.isUndoing) + { + if (items.count == 1) + { + [undo setActionName:NSLocalizedString(@"Move Job in Queue", @"Queue undo action name")]; + } + else + { + [undo setActionName:NSLocalizedString(@"Move Jobs in Queue", @"Queue undo action name")]; + } + } + + [self.items commit]; +} + +- (void)moveQueueItemsAtIndexes:(NSArray *)source toIndexes:(NSArray *)dest +{ + [self.items beginTransaction]; + + NSMutableArray<NSNumber *> *newSource = [NSMutableArray array]; + NSMutableArray<NSNumber *> *newDest = [NSMutableArray array]; + + for (NSInteger idx = source.count - 1; idx >= 0; idx--) + { + NSUInteger sourceIndex = [source[idx] integerValue]; + NSUInteger destIndex = [dest[idx] integerValue]; + + [newSource addObject:@(destIndex)]; + [newDest addObject:@(sourceIndex)]; + + id obj = [self.items objectAtIndex:sourceIndex]; + [self.items removeObjectAtIndex:sourceIndex]; + [self.items insertObject:obj atIndex:destIndex]; + } + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidMoveItemNotification + object:self + userInfo:@{HBQueueItemNotificationSourceIndexesKey: newDest, + HBQueueItemNotificationTargetIndexesKey: newSource}]; + + NSUndoManager *undo = self.undoManager; + [[undo prepareWithInvocationTarget:self] moveQueueItemsAtIndexes:newSource toIndexes:newDest]; + + if (!undo.isUndoing) + { + if (source.count == 1) + { + [undo setActionName:NSLocalizedString(@"Move Job in Queue", @"Queue undo action name")]; + } + else + { + [undo setActionName:NSLocalizedString(@"Move Jobs in Queue", @"Queue undo action name")]; + } + } + + [self.items commit]; +} + +/** + * This method will clear the queue of any encodes that are not still pending + * this includes both successfully completed encodes as well as canceled encodes + */ +- (void)removeCompletedAndCancelledItems +{ + [self.items beginTransaction]; + NSIndexSet *indexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { + return (item.state == HBQueueItemStateCompleted || item.state == HBQueueItemStateCanceled); + }]; + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidRemoveItemNotification object:self userInfo:@{@"indexes": indexes}]; + [self.items commit]; +} + +/** + * This method will clear the queue of all encodes. effectively creating an empty queue + */ +- (void)removeAllItems +{ + [self.items beginTransaction]; + + [self removeQueueItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.items.count)]]; + [self.items commit]; +} + +- (void)removeNotWorkingItems +{ + [self.items beginTransaction]; + NSIndexSet *indexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { + return (item.state != HBQueueItemStateWorking); + }]; + [self removeQueueItemsAtIndexes:indexes]; + [self.items commit]; +} + +- (void)removeCompletedItems +{ + [self.items beginTransaction]; + NSIndexSet *indexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { + return (item.state == HBQueueItemStateCompleted); + }]; + [self removeQueueItemsAtIndexes:indexes]; + [self.items commit]; +} + +- (void)resetItemsStateAtIndexes:(NSIndexSet *)indexes +{ + if ([self.items beginTransaction] == HBDistributedArrayContentReload) + { + // Do not execture the action if the array changed. + [self.items commit]; + return; + } + + NSMutableIndexSet *updatedIndexes = [NSMutableIndexSet indexSet]; + + NSUInteger currentIndex = indexes.firstIndex; + while (currentIndex != NSNotFound) { + HBQueueItem *item = self.items[currentIndex]; + + if (item.state == HBQueueItemStateCanceled || item.state == HBQueueItemStateCompleted || item.state == HBQueueItemStateFailed) + { + item.state = HBQueueItemStateReady; + [updatedIndexes addIndex:currentIndex]; + } + currentIndex = [indexes indexGreaterThanIndex:currentIndex]; + } + + [self updateStats]; + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidChangeItemNotification object:self userInfo:@{HBQueueItemNotificationIndexesKey: indexes}]; + [self.items commit]; +} + +- (void)resetAllItems +{ + [self.items beginTransaction]; + NSIndexSet *indexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { + return (item.state != HBQueueItemStateWorking); + }]; + [self resetItemsStateAtIndexes:indexes]; + [self.items commit]; +} + +- (void)resetFailedItems +{ + [self.items beginTransaction]; + NSIndexSet *indexes = [self.items indexesOfObjectsUsingBlock:^BOOL(HBQueueItem *item) { + return (item.state == HBQueueItemStateFailed); + }]; + [self resetItemsStateAtIndexes:indexes]; + [self.items commit]; +} + +/** + * This method will set any item marked as encoding back to pending + * currently used right after a queue reload + */ +- (void)setEncodingJobsAsPending +{ + [self.items beginTransaction]; + + NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet]; + NSUInteger idx = 0; + for (HBQueueItem *item in self.items) + { + // We want to keep any queue item that is pending or was previously being encoded + if (item.state == HBQueueItemStateWorking) + { + item.state = HBQueueItemStateReady; + [indexes addIndex:idx]; + } + idx++; + } + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidChangeItemNotification object:self userInfo:@{HBQueueItemNotificationIndexesKey: indexes}]; + [self.items commit]; +} + +- (BOOL)canEncode +{ + return self.pendingItemsCount > 0; +} + +- (BOOL)isEncoding +{ + HBState s = self.core.state; + return (s == HBStateScanning) || (s == HBStatePaused) || (s == HBStateWorking) || (s == HBStateMuxing) || (s == HBStateSearching); +} + +- (BOOL)canPause +{ + HBState s = self.core.state; + return (s == HBStateWorking || s == HBStateMuxing); +} + +- (void)pause +{ + [self.core pause]; + [self.core allowSleep]; +} + +- (BOOL)canResume +{ + return self.core.state == HBStatePaused; +} + +- (void)resume +{ + [self.core resume]; + [self.core preventSleep]; +} + +#pragma mark - Private queue editing methods + +/** + * Reloads the queue, this is called + * when another HandBrake instances modifies the queue + */ +- (void)reloadQueue +{ + [self updateStats]; + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueReloadItemsNotification object:self]; +} + +- (void)updateStats +{ + // lets get the stats on the status of the queue array + NSUInteger pendingCount = 0; + NSUInteger completedCount = 0; + + for (HBQueueItem *item in self.items) + { + if (item.state == HBQueueItemStateReady) + { + pendingCount++; + } + if (item.state == HBQueueItemStateCompleted) + { + completedCount++; + } + } + + self.pendingItemsCount = pendingCount; + self.completedItemsCount = completedCount; +} + +- (BOOL)_isDiskSpaceLowAtURL:(NSURL *)url +{ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HBQueuePauseIfLowSpace"]) + { + NSURL *volumeURL = nil; + NSDictionary<NSURLResourceKey, id> *attrs = [url resourceValuesForKeys:@[NSURLIsVolumeKey, NSURLVolumeURLKey] error:NULL]; + long long minCapacity = [[[NSUserDefaults standardUserDefaults] stringForKey:@"HBQueueMinFreeSpace"] longLongValue] * 1000000000; + + volumeURL = [attrs[NSURLIsVolumeKey] boolValue] ? url : attrs[NSURLVolumeURLKey]; + + if (volumeURL) + { + [volumeURL removeCachedResourceValueForKey:NSURLVolumeAvailableCapacityKey]; + attrs = [volumeURL resourceValuesForKeys:@[NSURLVolumeAvailableCapacityKey] error:NULL]; + + if (attrs[NSURLVolumeAvailableCapacityKey]) + { + if ([attrs[NSURLVolumeAvailableCapacityKey] longLongValue] < minCapacity) + { + return YES; + } + } + } + } + + return NO; +} + +/** + * Used to get the next pending queue item and return it if found + */ +- (HBQueueItem *)getNextPendingQueueItem +{ + for (HBQueueItem *item in self.items) + { + if (item.state == HBQueueItemStateReady) + { + return item; + } + } + return nil; +} + +- (void)start +{ + if (self.canEncode && self.core.state == HBStateIdle) + { + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidStartNotification object:self]; + [self.core preventSleep]; + [self encodeNextQueueItem]; + } +} + +/** + * Starts the queue + */ +- (void)encodeNextQueueItem +{ + [self.items beginTransaction]; + self.currentItem = nil; + + // since we have completed an encode, we go to the next + if (self.stop) + { + [HBUtilities writeToActivityLog:"Queue manually stopped"]; + + self.stop = NO; + [self.core allowSleep]; + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidCompleteNotification object:self]; + } + else + { + // Check to see if there are any more pending items in the queue + HBQueueItem *nextItem = [self getNextPendingQueueItem]; + + if (nextItem && [self _isDiskSpaceLowAtURL:nextItem.outputURL]) + { + // Disk space is low, show an alert + [HBUtilities writeToActivityLog:"Queue Stopped, low space on destination disk"]; + [self.core allowSleep]; + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidCompleteNotification object:self]; + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueLowSpaceAlertNotification object:self]; + } + // If we still have more pending items in our queue, lets go to the next one + else if (nextItem) + { + // now we mark the queue item as working so another instance can not come along and try to scan it while we are scanning + nextItem.state = HBQueueItemStateWorking; + + // Tell HB to output a new activity log file for this encode + self.currentLog = [[HBJobOutputFileWriter alloc] initWithJob:nextItem.job]; + if (self.currentLog) + { + [[HBOutputRedirect stderrRedirect] addListener:self.currentLog]; + [[HBOutputRedirect stdoutRedirect] addListener:self.currentLog]; + } + + self.currentItem = nextItem; + NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:[self.items indexOfObject:nextItem]]; + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidChangeItemNotification object:self userInfo:@{HBQueueItemNotificationIndexesKey: indexes}]; + + [self updateStats]; + + // now we can go ahead and scan the new pending queue item + [self encodeItem:nextItem]; + + // erase undo manager history + [self.undoManager removeAllActions]; + } + else + { + [HBUtilities writeToActivityLog:"Queue Done, there are no more pending encodes"]; + [self.core allowSleep]; + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidCompleteNotification object:self]; + } + } + [self.items commit]; +} + +- (void)completedItem:(HBQueueItem *)item result:(HBCoreResult)result; +{ + NSParameterAssert(item); + [self.items beginTransaction]; + + // Since we are done with this encode, tell output to stop writing to the + // individual encode log. + [[HBOutputRedirect stderrRedirect] removeListener:self.currentLog]; + [[HBOutputRedirect stdoutRedirect] removeListener:self.currentLog]; + + self.currentLog = nil; + + // Mark the encode just finished + switch (result) { + case HBCoreResultDone: + item.state = HBQueueItemStateCompleted; + break; + case HBCoreResultCanceled: + item.state = HBQueueItemStateCanceled; + break; + default: + item.state = HBQueueItemStateFailed; + break; + } + + // Update UI + NSString *info = nil; + switch (result) { + case HBCoreResultDone: + info = NSLocalizedString(@"Encode Finished.", @"Queue status"); + break; + case HBCoreResultCanceled: + info = NSLocalizedString(@"Encode Canceled.", @"Queue status"); + break; + default: + info = NSLocalizedString(@"Encode Failed.", @"Queue status"); + break; + } + + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueProgressNotification object:self userInfo:@{HBQueueProgressNotificationPercentKey: @1.0, + HBQueueProgressNotificationInfoKey: info}]; + + NSInteger index = [self.items indexOfObject:item]; + NSIndexSet *indexes = index > -1 ? [NSIndexSet indexSetWithIndex:index] : [NSIndexSet indexSet]; + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueDidCompleteItemNotification object:self userInfo:@{HBQueueDidCompleteItemNotificationItemKey: item, + HBQueueItemNotificationIndexesKey: indexes}]; + + [self.items commit]; +} + +/** + * Here we actually tell hb_scan to perform the source scan, using the path to source and title number + */ +- (void)encodeItem:(HBQueueItem *)item +{ + NSParameterAssert(item); + + // Progress handler + void (^progressHandler)(HBState state, HBProgress progress, NSString *info) = ^(HBState state, HBProgress progress, NSString *info) + { + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueProgressNotification object:self userInfo:@{HBQueueProgressNotificationPercentKey: @0, + HBQueueProgressNotificationInfoKey: info}]; + }; + + // Completion handler + void (^completionHandler)(HBCoreResult result) = ^(HBCoreResult result) + { + if (result == HBCoreResultDone) + { + [self realEncodeItem:item]; + } + else + { + [self completedItem:item result:result]; + [self encodeNextQueueItem]; + } + }; + + // Only scan 10 previews before an encode - additional previews are + // only useful for autocrop and static previews, which are already taken care of at this point + [self.core scanURL:item.fileURL + titleIndex:item.job.titleIdx + previews:10 + minDuration:0 + progressHandler:progressHandler + completionHandler:completionHandler]; +} + +/** + * This assumes that we have re-scanned and loaded up a new queue item to send to libhb + */ +- (void)realEncodeItem:(HBQueueItem *)item +{ + NSParameterAssert(item); + + HBJob *job = item.job; + + // Reset the title in the job. + job.title = self.core.titles.firstObject; + + NSParameterAssert(job); + + HBStateFormatter *formatter = [[HBStateFormatter alloc] init]; + formatter.title = job.outputFileName; + self.core.stateFormatter = formatter; + + // Progress handler + void (^progressHandler)(HBState state, HBProgress progress, NSString *info) = ^(HBState state, HBProgress progress, NSString *info) + { + if (state == HBStateMuxing) + { + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueProgressNotification + object:self + userInfo:@{HBQueueProgressNotificationPercentKey: @1, + HBQueueProgressNotificationInfoKey: info}]; + } + else + { + [NSNotificationCenter.defaultCenter postNotificationName:HBQueueProgressNotification + object:self + userInfo:@{HBQueueProgressNotificationPercentKey: @(progress.percent), + HBQueueProgressNotificationInfoKey: info}]; + } + }; + + // Completion handler + void (^completionHandler)(HBCoreResult result) = ^(HBCoreResult result) + { + [self completedItem:item result:result]; + [self encodeNextQueueItem]; + }; + + // We should be all setup so let 'er rip + [self.core encodeJob:job progressHandler:progressHandler completionHandler:completionHandler]; + + // We are done using the title, remove it from the job + job.title = nil; +} + +/** + * Cancels the current job + */ +- (void)doCancelCurrentItem +{ + if (self.core.state == HBStateScanning) + { + [self.core cancelScan]; + } + else + { + [self.core cancelEncode]; + } +} + +/** + * Cancels the current job and starts processing the next in queue. + */ +- (void)cancelCurrentItemAndContinue +{ + [self doCancelCurrentItem]; +} + +/** + * Cancels the current job and stops libhb from processing the remaining encodes. + */ +- (void)cancelCurrentItemAndStop +{ + self.stop = YES; + [self doCancelCurrentItem]; +} + +/** + * Finishes the current job and stops libhb from processing the remaining encodes. + */ +- (void)finishCurrentAndStop +{ + self.stop = YES; +} + +@end |