// This file Copyright © 2006-2022 Transmission authors and contributors. // It may be used under the MIT (SPDX: MIT) license. // License text can be found in the licenses/ folder. #include #include #include #include #include #include // tr_free() #import "Torrent.h" #import "GroupsController.h" #import "FileListNode.h" #import "NSApplicationAdditions.h" #import "NSStringAdditions.h" #import "TrackerNode.h" #define ETA_IDLE_DISPLAY_SEC (2 * 60) @interface Torrent () @property(nonatomic, readonly) tr_torrent* fHandle; @property(nonatomic) tr_stat const* fStat; @property(nonatomic, readonly) NSUserDefaults* fDefaults; @property(nonatomic) NSImage* fIcon; @property(nonatomic, copy) NSArray* fileList; @property(nonatomic, copy) NSArray* flatFileList; @property(nonatomic, copy) NSIndexSet* fPreviousFinishedIndexes; @property(nonatomic) NSDate* fPreviousFinishedIndexesDate; @property(nonatomic) NSInteger groupValue; @property(nonatomic) TorrentDeterminationType fGroupValueDetermination; @property(nonatomic) TorrentDeterminationType fDownloadFolderDetermination; @property(nonatomic) BOOL fResumeOnWake; @property(nonatomic) BOOL fTimeMachineExcludeInitialized; - (instancetype)initWithPath:(NSString*)path hash:(NSString*)hashString torrentStruct:(tr_torrent*)torrentStruct magnetAddress:(NSString*)magnetAddress lib:(tr_session*)lib groupValue:(NSNumber*)groupValue removeWhenFinishSeeding:(NSNumber*)removeWhenFinishSeeding downloadFolder:(NSString*)downloadFolder legacyIncompleteFolder:(NSString*)incompleteFolder; - (void)createFileList; - (void)insertPathForComponents:(NSArray*)components withComponentIndex:(NSUInteger)componentIndex forParent:(FileListNode*)parent fileSize:(uint64_t)size index:(NSInteger)index flatList:(NSMutableArray*)flatFileList; - (void)sortFileList:(NSMutableArray*)fileNodes; - (void)startQueue; - (void)completenessChange:(tr_completeness)status wasRunning:(BOOL)wasRunning; - (void)ratioLimitHit; - (void)idleLimitHit; - (void)metadataRetrieved; - (void)renameFinished:(BOOL)success nodes:(NSArray*)nodes completionHandler:(void (^)(BOOL))completionHandler oldPath:(NSString*)oldPath newName:(NSString*)newName; @property(nonatomic, readonly) BOOL shouldShowEta; @property(nonatomic, readonly) NSString* etaString; - (void)setTimeMachineExclude:(BOOL)exclude; @end void startQueueCallback(tr_torrent* torrent, void* torrentData) { dispatch_async(dispatch_get_main_queue(), ^{ [(__bridge Torrent*)torrentData startQueue]; }); } void completenessChangeCallback(tr_torrent* torrent, tr_completeness status, bool wasRunning, void* torrentData) { dispatch_async(dispatch_get_main_queue(), ^{ [(__bridge Torrent*)torrentData completenessChange:status wasRunning:wasRunning]; }); } void ratioLimitHitCallback(tr_torrent* torrent, void* torrentData) { dispatch_async(dispatch_get_main_queue(), ^{ [(__bridge Torrent*)torrentData ratioLimitHit]; }); } void idleLimitHitCallback(tr_torrent* torrent, void* torrentData) { dispatch_async(dispatch_get_main_queue(), ^{ [(__bridge Torrent*)torrentData idleLimitHit]; }); } void metadataCallback(tr_torrent* torrent, void* torrentData) { dispatch_async(dispatch_get_main_queue(), ^{ [(__bridge Torrent*)torrentData metadataRetrieved]; }); } void renameCallback(tr_torrent* torrent, char const* oldPathCharString, char const* newNameCharString, int error, void* contextInfo) { @autoreleasepool { NSString* oldPath = @(oldPathCharString); NSString* newName = @(newNameCharString); dispatch_async(dispatch_get_main_queue(), ^{ NSDictionary* contextDict = (__bridge_transfer NSDictionary*)contextInfo; Torrent* torrentObject = contextDict[@"Torrent"]; [torrentObject renameFinished:error == 0 nodes:contextDict[@"Nodes"] completionHandler:contextDict[@"CompletionHandler"] oldPath:oldPath newName:newName]; }); } } bool trashDataFile(char const* filename, tr_error** error) { if (filename == NULL) { return false; } @autoreleasepool { NSError* localError; if (![Torrent trashFile:@(filename) error:&localError]) { tr_error_set(error, localError.code, localError.description.UTF8String); return false; } } return true; } @implementation Torrent - (instancetype)initWithPath:(NSString*)path location:(NSString*)location deleteTorrentFile:(BOOL)torrentDelete lib:(tr_session*)lib { self = [self initWithPath:path hash:nil torrentStruct:NULL magnetAddress:nil lib:lib groupValue:nil removeWhenFinishSeeding:nil downloadFolder:location legacyIncompleteFolder:nil]; if (self) { if (torrentDelete && ![self.torrentLocation isEqualToString:path]) { [Torrent trashFile:path error:nil]; } } return self; } - (instancetype)initWithTorrentStruct:(tr_torrent*)torrentStruct location:(NSString*)location lib:(tr_session*)lib { self = [self initWithPath:nil hash:nil torrentStruct:torrentStruct magnetAddress:nil lib:lib groupValue:nil removeWhenFinishSeeding:nil downloadFolder:location legacyIncompleteFolder:nil]; return self; } - (instancetype)initWithMagnetAddress:(NSString*)address location:(NSString*)location lib:(tr_session*)lib { self = [self initWithPath:nil hash:nil torrentStruct:nil magnetAddress:address lib:lib groupValue:nil removeWhenFinishSeeding:nil downloadFolder:location legacyIncompleteFolder:nil]; return self; } - (void)setResumeStatusForTorrent:(Torrent*)torrent withHistory:(NSDictionary*)history forcePause:(BOOL)pause { //restore GroupValue torrent.groupValue = [history[@"GroupValue"] intValue]; //start transfer NSNumber* active; if (!pause && (active = history[@"Active"]) && active.boolValue) { [torrent startTransferNoQueue]; } NSNumber* ratioLimit; if ((ratioLimit = history[@"RatioLimit"])) { self.ratioLimit = ratioLimit.floatValue; } } - (NSDictionary*)history { return @{ @"TorrentHash" : self.hashString, @"Active" : @(self.active), @"WaitToStart" : @(self.waitingToStart), @"GroupValue" : @(self.groupValue), @"RemoveWhenFinishSeeding" : @(_removeWhenFinishSeeding) }; } - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self]; } - (NSString*)description { return [@"Torrent: " stringByAppendingString:self.name]; } - (id)copyWithZone:(NSZone*)zone { return self; } - (void)closeRemoveTorrent:(BOOL)trashFiles { //allow the file to be indexed by Time Machine [self setTimeMachineExclude:NO]; tr_torrentRemove(self.fHandle, trashFiles, trashDataFile); } - (void)changeDownloadFolderBeforeUsing:(NSString*)folder determinationType:(TorrentDeterminationType)determinationType { //if data existed in original download location, unexclude it before changing the location [self setTimeMachineExclude:NO]; tr_torrentSetDownloadDir(self.fHandle, folder.UTF8String); self.fDownloadFolderDetermination = determinationType; } - (NSString*)currentDirectory { return @(tr_torrentGetCurrentDir(self.fHandle)); } - (void)getAvailability:(int8_t*)tab size:(NSInteger)size { tr_torrentAvailability(self.fHandle, tab, size); } - (void)getAmountFinished:(float*)tab size:(NSInteger)size { tr_torrentAmountFinished(self.fHandle, tab, size); } - (NSIndexSet*)previousFinishedPieces { //if the torrent hasn't been seen in a bit, and therefore hasn't been refreshed, return nil if (self.fPreviousFinishedIndexesDate && self.fPreviousFinishedIndexesDate.timeIntervalSinceNow > -2.0) { return self.fPreviousFinishedIndexes; } else { return nil; } } - (void)setPreviousFinishedPieces:(NSIndexSet*)indexes { self.fPreviousFinishedIndexes = indexes; self.fPreviousFinishedIndexesDate = indexes != nil ? [[NSDate alloc] init] : nil; } - (void)update { //get previous stalled value before update BOOL const wasStalled = self.fStat != NULL && self.stalled; self.fStat = tr_torrentStat(self.fHandle); //make sure the "active" filter is updated when stalled-ness changes if (wasStalled != self.stalled) { //posting asynchronously with coalescing to prevent stack overflow on lots of torrents changing state at the same time [NSNotificationQueue.defaultQueue enqueueNotification:[NSNotification notificationWithName:@"UpdateQueue" object:self] postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:nil]; } //when the torrent is first loaded, update the time machine exclusion if (!self.fTimeMachineExcludeInitialized) { [self updateTimeMachineExclude]; } } - (void)startTransferIgnoringQueue:(BOOL)ignoreQueue { if ([self alertForRemainingDiskSpace]) { ignoreQueue ? tr_torrentStartNow(self.fHandle) : tr_torrentStart(self.fHandle); [self update]; //capture, specifically, stop-seeding settings changing to unlimited [NSNotificationCenter.defaultCenter postNotificationName:@"UpdateOptions" object:nil]; } } - (void)startTransferNoQueue { [self startTransferIgnoringQueue:YES]; } - (void)startTransfer { [self startTransferIgnoringQueue:NO]; } - (void)stopTransfer { tr_torrentStop(self.fHandle); [self update]; } - (void)sleep { if ((self.fResumeOnWake = self.active)) { tr_torrentStop(self.fHandle); } } - (void)wakeUp { if (self.fResumeOnWake) { tr_logAddTrace("restarting because of wakeup", tr_torrentName(self.fHandle)); tr_torrentStart(self.fHandle); } } - (NSUInteger)queuePosition { return self.fStat->queuePosition; } - (void)setQueuePosition:(NSUInteger)index { tr_torrentSetQueuePosition(self.fHandle, index); } - (void)manualAnnounce { tr_torrentManualUpdate(self.fHandle); } - (BOOL)canManualAnnounce { return tr_torrentCanManualUpdate(self.fHandle); } - (void)resetCache { tr_torrentVerify(self.fHandle); [self update]; } - (BOOL)isMagnet { return !tr_torrentHasMetadata(self.fHandle); } - (NSString*)magnetLink { return @(tr_torrentGetMagnetLink(self.fHandle)); } - (CGFloat)ratio { return self.fStat->ratio; } - (tr_ratiolimit)ratioSetting { return tr_torrentGetRatioMode(self.fHandle); } - (void)setRatioSetting:(tr_ratiolimit)setting { tr_torrentSetRatioMode(self.fHandle, setting); } - (CGFloat)ratioLimit { return tr_torrentGetRatioLimit(self.fHandle); } - (void)setRatioLimit:(CGFloat)limit { NSParameterAssert(limit >= 0); tr_torrentSetRatioLimit(self.fHandle, limit); } - (CGFloat)progressStopRatio { return self.fStat->seedRatioPercentDone; } - (tr_idlelimit)idleSetting { return tr_torrentGetIdleMode(self.fHandle); } - (void)setIdleSetting:(tr_idlelimit)setting { tr_torrentSetIdleMode(self.fHandle, setting); } - (NSUInteger)idleLimitMinutes { return tr_torrentGetIdleLimit(self.fHandle); } - (void)setIdleLimitMinutes:(NSUInteger)limit { NSParameterAssert(limit > 0); tr_torrentSetIdleLimit(self.fHandle, limit); } - (BOOL)usesSpeedLimit:(BOOL)upload { return tr_torrentUsesSpeedLimit(self.fHandle, upload ? TR_UP : TR_DOWN); } - (void)setUseSpeedLimit:(BOOL)use upload:(BOOL)upload { tr_torrentUseSpeedLimit(self.fHandle, upload ? TR_UP : TR_DOWN, use); } - (NSInteger)speedLimit:(BOOL)upload { return tr_torrentGetSpeedLimit_KBps(self.fHandle, upload ? TR_UP : TR_DOWN); } - (void)setSpeedLimit:(NSInteger)limit upload:(BOOL)upload { tr_torrentSetSpeedLimit_KBps(self.fHandle, upload ? TR_UP : TR_DOWN, limit); } - (BOOL)usesGlobalSpeedLimit { return tr_torrentUsesSessionLimits(self.fHandle); } - (void)setUseGlobalSpeedLimit:(BOOL)use { tr_torrentUseSessionLimits(self.fHandle, use); } - (void)setMaxPeerConnect:(uint16_t)count { NSParameterAssert(count > 0); tr_torrentSetPeerLimit(self.fHandle, count); } - (uint16_t)maxPeerConnect { return tr_torrentGetPeerLimit(self.fHandle); } - (BOOL)waitingToStart { return self.fStat->activity == TR_STATUS_DOWNLOAD_WAIT || self.fStat->activity == TR_STATUS_SEED_WAIT; } - (tr_priority_t)priority { return tr_torrentGetPriority(self.fHandle); } - (void)setPriority:(tr_priority_t)priority { return tr_torrentSetPriority(self.fHandle, priority); } + (BOOL)trashFile:(NSString*)path error:(NSError**)error { // Attempt to move to trash if ([NSFileManager.defaultManager trashItemAtURL:[NSURL fileURLWithPath:path] resultingItemURL:nil error:nil]) { NSLog(@"Old moved to Trash %@", path); return YES; } // If cannot trash, just delete it (will work if it's on a remote volume) NSError* localError; if ([NSFileManager.defaultManager removeItemAtPath:path error:&localError]) { NSLog(@"Old removed %@", path); return YES; } NSLog(@"Old could not be trashed or removed %@: %@", path, localError.localizedDescription); if (error != nil) { *error = localError; } return NO; } - (void)moveTorrentDataFileTo:(NSString*)folder { NSString* oldFolder = self.currentDirectory; if ([oldFolder isEqualToString:folder]) { return; } //check if moving inside itself NSArray *oldComponents = oldFolder.pathComponents, *newComponents = folder.pathComponents; NSUInteger const oldCount = oldComponents.count; if (oldCount < newComponents.count && [newComponents[oldCount] isEqualToString:self.name] && [folder hasPrefix:oldFolder]) { NSAlert* alert = [[NSAlert alloc] init]; alert.messageText = NSLocalizedString(@"A folder cannot be moved to inside itself.", "Move inside itself alert -> title"); alert.informativeText = [NSString stringWithFormat:NSLocalizedString(@"The move operation of \"%@\" cannot be done.", "Move inside itself alert -> message"), self.name]; [alert addButtonWithTitle:NSLocalizedString(@"OK", "Move inside itself alert -> button")]; [alert runModal]; return; } volatile int status; tr_torrentSetLocation(self.fHandle, folder.UTF8String, YES, NULL, &status); while (status == TR_LOC_MOVING) //block while moving (for now) { [NSThread sleepForTimeInterval:0.05]; } if (status == TR_LOC_DONE) { [NSNotificationCenter.defaultCenter postNotificationName:@"UpdateStats" object:nil]; } else { NSAlert* alert = [[NSAlert alloc] init]; alert.messageText = NSLocalizedString(@"There was an error moving the data file.", "Move error alert -> title"); alert.informativeText = [NSString stringWithFormat:NSLocalizedString(@"The move operation of \"%@\" cannot be done.", "Move error alert -> message"), self.name]; [alert addButtonWithTitle:NSLocalizedString(@"OK", "Move error alert -> button")]; [alert runModal]; } [self updateTimeMachineExclude]; } - (void)copyTorrentFileTo:(NSString*)path { [NSFileManager.defaultManager copyItemAtPath:self.torrentLocation toPath:path error:NULL]; } - (BOOL)alertForRemainingDiskSpace { if (self.allDownloaded || ![self.fDefaults boolForKey:@"WarningRemainingSpace"]) { return YES; } NSString* downloadFolder = self.currentDirectory; NSDictionary* systemAttributes; if ((systemAttributes = [NSFileManager.defaultManager attributesOfFileSystemForPath:downloadFolder error:NULL])) { uint64_t const remainingSpace = ((NSNumber*)systemAttributes[NSFileSystemFreeSize]).unsignedLongLongValue; //if the remaining space is greater than the size left, then there is enough space regardless of preallocation if (remainingSpace < self.sizeLeft && remainingSpace < tr_torrentGetBytesLeftToAllocate(self.fHandle)) { NSString* volumeName = [NSFileManager.defaultManager componentsToDisplayForPath:downloadFolder][0]; NSAlert* alert = [[NSAlert alloc] init]; alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Not enough remaining disk space to download \"%@\" completely.", "Torrent disk space alert -> title"), self.name]; alert.informativeText = [NSString stringWithFormat:NSLocalizedString( @"The transfer will be paused." " Clear up space on %@ or deselect files in the torrent inspector to continue.", "Torrent disk space alert -> message"), volumeName]; [alert addButtonWithTitle:NSLocalizedString(@"OK", "Torrent disk space alert -> button")]; [alert addButtonWithTitle:NSLocalizedString(@"Download Anyway", "Torrent disk space alert -> button")]; alert.showsSuppressionButton = YES; alert.suppressionButton.title = NSLocalizedString(@"Do not check disk space again", "Torrent disk space alert -> button"); NSInteger const result = [alert runModal]; if (alert.suppressionButton.state == NSControlStateValueOn) { [self.fDefaults setBool:NO forKey:@"WarningRemainingSpace"]; } return result != NSAlertFirstButtonReturn; } } return YES; } - (NSImage*)icon { if (self.magnet) { return [NSImage imageNamed:@"Magnet"]; } if (!self.fIcon) { self.fIcon = self.folder ? [NSImage imageNamed:NSImageNameFolder] : [NSWorkspace.sharedWorkspace iconForFileType:self.name.pathExtension]; } return self.fIcon; } - (NSString*)name { return @(tr_torrentName(self.fHandle)); } - (BOOL)isFolder { return tr_torrentView(self.fHandle).is_folder; } - (uint64_t)size { return tr_torrentTotalSize(self.fHandle); } - (uint64_t)sizeLeft { return self.fStat->leftUntilDone; } - (NSMutableArray*)allTrackerStats { auto const count = tr_torrentTrackerCount(self.fHandle); auto tier = std::optional{}; NSMutableArray* trackers = [NSMutableArray arrayWithCapacity:count * 2]; for (size_t i = 0; i < count; ++i) { auto const tracker = tr_torrentTracker(self.fHandle, i); if (!tier || tier != tracker.tier) { tier = tracker.tier; [trackers addObject:@{ @"Tier" : @(tracker.tier + 1), @"Name" : self.name }]; } auto* tracker_node = [[TrackerNode alloc] initWithTrackerView:&tracker torrent:self]; [trackers addObject:tracker_node]; } return trackers; } - (NSArray*)allTrackersFlat { auto const n = tr_torrentTrackerCount(self.fHandle); NSMutableArray* allTrackers = [NSMutableArray arrayWithCapacity:n]; for (size_t i = 0; i < n; ++i) { [allTrackers addObject:@(tr_torrentTracker(self.fHandle, i).announce)]; } return allTrackers; } - (BOOL)addTrackerToNewTier:(NSString*)new_tracker { new_tracker = [new_tracker stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; if ([new_tracker rangeOfString:@"://"].location == NSNotFound) { new_tracker = [@"http://" stringByAppendingString:new_tracker]; } char* old_list = tr_torrentGetTrackerList(self.fHandle); auto const new_list = fmt::format(FMT_STRING("{:s}\n\n{:s}"), old_list, new_tracker.UTF8String); BOOL const success = tr_torrentSetTrackerList(self.fHandle, new_list.c_str()); tr_free(old_list); return success; } - (void)removeTrackers:(NSSet*)trackers { auto new_list = std::string{}; auto current_tier = std::optional{}; for (size_t i = 0, n = tr_torrentTrackerCount(self.fHandle); i < n; ++i) { auto const tracker = tr_torrentTracker(self.fHandle, i); if ([trackers containsObject:@(tracker.announce)]) { continue; } if (current_tier && *current_tier != tracker.tier) { new_list += '\n'; } new_list += tracker.announce; new_list += '\n'; current_tier = tracker.tier; } BOOL const success = tr_torrentSetTrackerList(self.fHandle, new_list.c_str()); NSAssert(success, @"Removing tracker addresses failed"); } - (NSString*)comment { auto const* comment = tr_torrentView(self.fHandle).comment; return comment ? @(comment) : @""; } - (NSString*)creator { auto const* creator = tr_torrentView(self.fHandle).creator; return creator ? @(creator) : @""; } - (NSDate*)dateCreated { auto const date = tr_torrentView(self.fHandle).date_created; return date > 0 ? [NSDate dateWithTimeIntervalSince1970:date] : nil; } - (NSInteger)pieceSize { return tr_torrentView(self.fHandle).piece_size; } - (NSInteger)pieceCount { return tr_torrentView(self.fHandle).n_pieces; } - (NSString*)hashString { return @(tr_torrentView(self.fHandle).hash_string); } - (BOOL)privateTorrent { return tr_torrentView(self.fHandle).is_private; } - (NSString*)torrentLocation { auto* const filename = tr_torrentFilename(self.fHandle); NSString* ret = @(filename ? filename : ""); tr_free(filename); return ret; } - (NSString*)dataLocation { if (self.magnet) { return nil; } if (self.folder) { NSString* dataLocation = [self.currentDirectory stringByAppendingPathComponent:self.name]; if (![NSFileManager.defaultManager fileExistsAtPath:dataLocation]) { return nil; } return dataLocation; } else { char* location = tr_torrentFindFile(self.fHandle, 0); if (location == NULL) { return nil; } NSString* dataLocation = @(location); free(location); return dataLocation; } } - (NSString*)fileLocation:(FileListNode*)node { if (node.isFolder) { NSString* basePath = [node.path stringByAppendingPathComponent:node.name]; NSString* dataLocation = [self.currentDirectory stringByAppendingPathComponent:basePath]; if (![NSFileManager.defaultManager fileExistsAtPath:dataLocation]) { return nil; } return dataLocation; } else { char* location = tr_torrentFindFile(self.fHandle, node.indexes.firstIndex); if (location == NULL) { return nil; } NSString* dataLocation = @(location); free(location); return dataLocation; } } - (void)renameTorrent:(NSString*)newName completionHandler:(void (^)(BOOL didRename))completionHandler { NSParameterAssert(newName != nil); NSParameterAssert(![newName isEqualToString:@""]); NSDictionary* contextInfo = @{ @"Torrent" : self, @"CompletionHandler" : [completionHandler copy] }; tr_torrentRenamePath(self.fHandle, tr_torrentName(self.fHandle), newName.UTF8String, renameCallback, (__bridge_retained void*)(contextInfo)); } - (void)renameFileNode:(FileListNode*)node withName:(NSString*)newName completionHandler:(void (^)(BOOL didRename))completionHandler { NSParameterAssert(node.torrent == self); NSParameterAssert(newName != nil); NSParameterAssert(![newName isEqualToString:@""]); NSDictionary* contextInfo = @{ @"Torrent" : self, @"Nodes" : @[ node ], @"CompletionHandler" : [completionHandler copy] }; NSString* oldPath = [node.path stringByAppendingPathComponent:node.name]; tr_torrentRenamePath(self.fHandle, oldPath.UTF8String, newName.UTF8String, renameCallback, (__bridge_retained void*)(contextInfo)); } - (CGFloat)progress { return self.fStat->percentComplete; } - (CGFloat)progressDone { return self.fStat->percentDone; } - (CGFloat)progressLeft { if (self.size == 0) //magnet links { return 0.0; } return (CGFloat)self.sizeLeft / self.size; } - (CGFloat)checkingProgress { return self.fStat->recheckProgress; } - (CGFloat)availableDesired { return (CGFloat)self.fStat->desiredAvailable / self.sizeLeft; } - (BOOL)isActive { return self.fStat->activity != TR_STATUS_STOPPED && self.fStat->activity != TR_STATUS_DOWNLOAD_WAIT && self.fStat->activity != TR_STATUS_SEED_WAIT; } - (BOOL)isSeeding { return self.fStat->activity == TR_STATUS_SEED; } - (BOOL)isChecking { return self.fStat->activity == TR_STATUS_CHECK || self.fStat->activity == TR_STATUS_CHECK_WAIT; } - (BOOL)isCheckingWaiting { return self.fStat->activity == TR_STATUS_CHECK_WAIT; } - (BOOL)allDownloaded { return self.sizeLeft == 0 && !self.magnet; } - (BOOL)isComplete { return self.progress >= 1.0; } - (BOOL)isFinishedSeeding { return self.fStat->finished; } - (BOOL)isError { return self.fStat->error == TR_STAT_LOCAL_ERROR; } - (BOOL)isAnyErrorOrWarning { return self.fStat->error != TR_STAT_OK; } - (NSString*)errorMessage { if (!self.anyErrorOrWarning) { return @""; } NSString* error; if (!(error = @(self.fStat->errorString)) && !(error = [NSString stringWithCString:self.fStat->errorString encoding:NSISOLatin1StringEncoding])) { error = [NSString stringWithFormat:@"(%@)", NSLocalizedString(@"unreadable error", "Torrent -> error string unreadable")]; } //libtransmission uses "Set Location", Mac client uses "Move data file to..." - very hacky! error = [error stringByReplacingOccurrencesOfString:@"Set Location" withString:[@"Move Data File To" stringByAppendingEllipsis]]; return error; } - (NSArray*)peers { int totalPeers; tr_peer_stat* peers = tr_torrentPeers(self.fHandle, &totalPeers); NSMutableArray* peerDicts = [NSMutableArray arrayWithCapacity:totalPeers]; for (int i = 0; i < totalPeers; i++) { tr_peer_stat* peer = &peers[i]; NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:12]; dict[@"Name"] = self.name; dict[@"From"] = @(peer->from); dict[@"IP"] = @(peer->addr); dict[@"Port"] = @(peer->port); dict[@"Progress"] = @(peer->progress); dict[@"Seed"] = @(peer->isSeed); dict[@"Encryption"] = @(peer->isEncrypted); dict[@"uTP"] = @(peer->isUTP); dict[@"Client"] = @(peer->client); dict[@"Flags"] = @(peer->flagStr); if (peer->isUploadingTo) { dict[@"UL To Rate"] = @(peer->rateToPeer_KBps); } if (peer->isDownloadingFrom) { dict[@"DL From Rate"] = @(peer->rateToClient_KBps); } [peerDicts addObject:dict]; } tr_torrentPeersFree(peers, totalPeers); return peerDicts; } - (NSUInteger)webSeedCount { return tr_torrentWebseedCount(self.fHandle); } - (NSArray*)webSeeds { NSUInteger n = tr_torrentWebseedCount(self.fHandle); NSMutableArray* webSeeds = [NSMutableArray arrayWithCapacity:n]; for (NSUInteger i = 0; i < n; ++i) { auto const webseed = tr_torrentWebseed(self.fHandle, i); NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:3]; dict[@"Name"] = self.name; dict[@"Address"] = @(webseed.url); if (webseed.is_downloading) { dict[@"DL From Rate"] = @(double(webseed.download_bytes_per_second) / 1000); } [webSeeds addObject:dict]; } return webSeeds; } - (NSString*)progressString { if (self.magnet) { NSString* progressString = self.fStat->metadataPercentComplete > 0.0 ? [NSString stringWithFormat:NSLocalizedString(@"%@ of torrent metadata retrieved", "Torrent -> progress string"), [NSString percentString:self.fStat->metadataPercentComplete longDecimals:YES]] : NSLocalizedString(@"torrent metadata needed", "Torrent -> progress string"); return [NSString stringWithFormat:@"%@ - %@", NSLocalizedString(@"Magnetized transfer", "Torrent -> progress string"), progressString]; } NSString* string; if (!self.allDownloaded) { CGFloat progress; if (self.folder && [self.fDefaults boolForKey:@"DisplayStatusProgressSelected"]) { string = [NSString stringForFilePartialSize:self.haveTotal fullSize:self.totalSizeSelected]; progress = self.progressDone; } else { string = [NSString stringForFilePartialSize:self.haveTotal fullSize:self.size]; progress = self.progress; } string = [string stringByAppendingFormat:@" (%@)", [NSString percentString:progress longDecimals:YES]]; } else { NSString* downloadString; if (!self.complete) //only multifile possible { if ([self.fDefaults boolForKey:@"DisplayStatusProgressSelected"]) { downloadString = [NSString stringWithFormat:NSLocalizedString(@"%@ selected", "Torrent -> progress string"), [NSString stringForFileSize:self.haveTotal]]; } else { downloadString = [NSString stringForFilePartialSize:self.haveTotal fullSize:self.size]; downloadString = [downloadString stringByAppendingFormat:@" (%@)", [NSString percentString:self.progress longDecimals:YES]]; } } else { downloadString = [NSString stringForFileSize:self.size]; } NSString* uploadString = [NSString stringWithFormat:NSLocalizedString(@"uploaded %@ (Ratio: %@)", "Torrent -> progress string"), [NSString stringForFileSize:self.uploadedTotal], [NSString stringForRatio:self.ratio]]; string = [downloadString stringByAppendingFormat:@", %@", uploadString]; } //add time when downloading or seed limit set if (self.shouldShowEta) { string = [string stringByAppendingFormat:@" - %@", self.etaString]; } return string; } - (NSString*)statusString { NSString* string; if (self.anyErrorOrWarning) { switch (self.fStat->error) { case TR_STAT_LOCAL_ERROR: string = NSLocalizedString(@"Error", "Torrent -> status string"); break; case TR_STAT_TRACKER_ERROR: string = NSLocalizedString(@"Tracker returned error", "Torrent -> status string"); break; case TR_STAT_TRACKER_WARNING: string = NSLocalizedString(@"Tracker returned warning", "Torrent -> status string"); break; default: NSAssert(NO, @"unknown error state"); } NSString* errorString = self.errorMessage; if (errorString && ![errorString isEqualToString:@""]) { string = [string stringByAppendingFormat:@": %@", errorString]; } } else { switch (self.fStat->activity) { case TR_STATUS_STOPPED: if (self.finishedSeeding) { string = NSLocalizedString(@"Seeding complete", "Torrent -> status string"); } else { string = NSLocalizedString(@"Paused", "Torrent -> status string"); } break; case TR_STATUS_DOWNLOAD_WAIT: string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis]; break; case TR_STATUS_SEED_WAIT: string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis]; break; case TR_STATUS_CHECK_WAIT: string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis]; break; case TR_STATUS_CHECK: string = [NSString stringWithFormat:@"%@ (%@)", NSLocalizedString(@"Checking existing data", "Torrent -> status string"), [NSString percentString:self.checkingProgress longDecimals:YES]]; break; case TR_STATUS_DOWNLOAD: if (self.totalPeersConnected != 1) { string = [NSString stringWithFormat:NSLocalizedString(@"Downloading from %d of %d peers", "Torrent -> status string"), self.peersSendingToUs, self.totalPeersConnected]; } else { string = [NSString stringWithFormat:NSLocalizedString(@"Downloading from %d of 1 peer", "Torrent -> status string"), self.peersSendingToUs]; } if (NSInteger const webSeedCount = self.fStat->webseedsSendingToUs; webSeedCount > 0) { NSString* webSeedString; if (webSeedCount == 1) { webSeedString = NSLocalizedString(@"web seed", "Torrent -> status string"); } else { webSeedString = [NSString stringWithFormat:NSLocalizedString(@"%d web seeds", "Torrent -> status string"), webSeedCount]; } string = [string stringByAppendingFormat:@" + %@", webSeedString]; } break; case TR_STATUS_SEED: if (self.totalPeersConnected != 1) { string = [NSString stringWithFormat:NSLocalizedString(@"Seeding to %d of %d peers", "Torrent -> status string"), self.peersGettingFromUs, self.totalPeersConnected]; } else { string = [NSString stringWithFormat:NSLocalizedString(@"Seeding to %d of 1 peer", "Torrent -> status string"), self.peersGettingFromUs]; } } if (self.stalled) { string = [NSLocalizedString(@"Stalled", "Torrent -> status string") stringByAppendingFormat:@", %@", string]; } } //append even if error if (self.active && !self.checking) { if (self.fStat->activity == TR_STATUS_DOWNLOAD) { string = [string stringByAppendingFormat:@" - %@: %@, %@: %@", NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed:self.downloadRate], NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed:self.uploadRate]]; } else { string = [string stringByAppendingFormat:@" - %@: %@", NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed:self.uploadRate]]; } } return string; } - (NSString*)shortStatusString { NSString* string; switch (self.fStat->activity) { case TR_STATUS_STOPPED: if (self.finishedSeeding) { string = NSLocalizedString(@"Seeding complete", "Torrent -> status string"); } else { string = NSLocalizedString(@"Paused", "Torrent -> status string"); } break; case TR_STATUS_DOWNLOAD_WAIT: string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis]; break; case TR_STATUS_SEED_WAIT: string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis]; break; case TR_STATUS_CHECK_WAIT: string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis]; break; case TR_STATUS_CHECK: string = [NSString stringWithFormat:@"%@ (%@)", NSLocalizedString(@"Checking existing data", "Torrent -> status string"), [NSString percentString:self.checkingProgress longDecimals:YES]]; break; case TR_STATUS_DOWNLOAD: string = [NSString stringWithFormat:@"%@: %@, %@: %@", NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed:self.downloadRate], NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed:self.uploadRate]]; break; case TR_STATUS_SEED: string = [NSString stringWithFormat:@"%@: %@, %@: %@", NSLocalizedString(@"Ratio", "Torrent -> status string"), [NSString stringForRatio:self.ratio], NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed:self.uploadRate]]; } return string; } - (NSString*)remainingTimeString { if (self.shouldShowEta) { return self.etaString; } else { return self.shortStatusString; } } - (NSString*)stateString { switch (self.fStat->activity) { case TR_STATUS_STOPPED: case TR_STATUS_DOWNLOAD_WAIT: case TR_STATUS_SEED_WAIT: { NSString* string = NSLocalizedString(@"Paused", "Torrent -> status string"); NSString* extra = nil; if (self.waitingToStart) { extra = self.fStat->activity == TR_STATUS_DOWNLOAD_WAIT ? NSLocalizedString(@"Waiting to download", "Torrent -> status string") : NSLocalizedString(@"Waiting to seed", "Torrent -> status string"); } else if (self.finishedSeeding) { extra = NSLocalizedString(@"Seeding complete", "Torrent -> status string"); } return extra ? [string stringByAppendingFormat:@" (%@)", extra] : string; } case TR_STATUS_CHECK_WAIT: return [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis]; case TR_STATUS_CHECK: return [NSString stringWithFormat:@"%@ (%@)", NSLocalizedString(@"Checking existing data", "Torrent -> status string"), [NSString percentString:self.checkingProgress longDecimals:YES]]; case TR_STATUS_DOWNLOAD: return NSLocalizedString(@"Downloading", "Torrent -> status string"); case TR_STATUS_SEED: return NSLocalizedString(@"Seeding", "Torrent -> status string"); } } - (NSInteger)totalPeersConnected { return self.fStat->peersConnected; } - (NSInteger)totalPeersTracker { return self.fStat->peersFrom[TR_PEER_FROM_TRACKER]; } - (NSInteger)totalPeersIncoming { return self.fStat->peersFrom[TR_PEER_FROM_INCOMING]; } - (NSInteger)totalPeersCache { return self.fStat->peersFrom[TR_PEER_FROM_RESUME]; } - (NSInteger)totalPeersPex { return self.fStat->peersFrom[TR_PEER_FROM_PEX]; } - (NSInteger)totalPeersDHT { return self.fStat->peersFrom[TR_PEER_FROM_DHT]; } - (NSInteger)totalPeersLocal { return self.fStat->peersFrom[TR_PEER_FROM_LPD]; } - (NSInteger)totalPeersLTEP { return self.fStat->peersFrom[TR_PEER_FROM_LTEP]; } - (NSInteger)peersSendingToUs { return self.fStat->peersSendingToUs; } - (NSInteger)peersGettingFromUs { return self.fStat->peersGettingFromUs; } - (CGFloat)downloadRate { return self.fStat->pieceDownloadSpeed_KBps; } - (CGFloat)uploadRate { return self.fStat->pieceUploadSpeed_KBps; } - (CGFloat)totalRate { return self.downloadRate + self.uploadRate; } - (uint64_t)haveVerified { return self.fStat->haveValid; } - (uint64_t)haveTotal { return self.haveVerified + self.fStat->haveUnchecked; } - (uint64_t)totalSizeSelected { return self.fStat->sizeWhenDone; } - (uint64_t)downloadedTotal { return self.fStat->downloadedEver; } - (uint64_t)uploadedTotal { return self.fStat->uploadedEver; } - (uint64_t)failedHash { return self.fStat->corruptEver; } - (void)setGroupValue:(NSInteger)groupValue determinationType:(TorrentDeterminationType)determinationType { if (groupValue != self.groupValue) { self.groupValue = groupValue; [NSNotificationCenter.defaultCenter postNotificationName:kTorrentDidChangeGroupNotification object:self]; } self.fGroupValueDetermination = determinationType; } - (NSInteger)groupOrderValue { return [GroupsController.groups rowValueForIndex:self.groupValue]; } - (void)checkGroupValueForRemoval:(NSNotification*)notification { if (self.groupValue != -1 && [notification.userInfo[@"Index"] integerValue] == self.groupValue) { self.groupValue = -1; } } - (NSInteger)fileCount { return tr_torrentFileCount(self.fHandle); } - (CGFloat)fileProgress:(FileListNode*)node { if (self.fileCount == 1 || self.complete) { return self.progress; } // #5501 if (node.size == 0) { return 1.0; } uint64_t have = 0; NSIndexSet* indexSet = node.indexes; for (NSInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index]) { have += tr_torrentFile(self.fHandle, index).have; } return (CGFloat)have / node.size; } - (BOOL)canChangeDownloadCheckForFile:(NSUInteger)index { NSAssert2((NSInteger)index < self.fileCount, @"Index %ld is greater than file count %ld", index, self.fileCount); return [self canChangeDownloadCheckForFiles:[NSIndexSet indexSetWithIndex:index]]; } - (BOOL)canChangeDownloadCheckForFiles:(NSIndexSet*)indexSet { if (self.fileCount == 1 || self.complete) { return NO; } __block BOOL canChange = NO; [indexSet enumerateIndexesWithOptions:NSEnumerationConcurrent usingBlock:^(NSUInteger index, BOOL* stop) { auto const file = tr_torrentFile(self.fHandle, index); if (file.have < file.length) { canChange = YES; *stop = YES; } }]; return canChange; } - (NSInteger)checkForFiles:(NSIndexSet*)indexSet { BOOL onState = NO, offState = NO; for (NSUInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index]) { auto const file = tr_torrentFile(self.fHandle, index); if (file.wanted || ![self canChangeDownloadCheckForFile:index]) { onState = YES; } else { offState = YES; } if (onState && offState) { return NSControlStateValueMixed; } } return onState ? NSControlStateValueOn : NSControlStateValueOff; } - (void)setFileCheckState:(NSInteger)state forIndexes:(NSIndexSet*)indexSet { NSUInteger count = indexSet.count; tr_file_index_t* files = static_cast(malloc(count * sizeof(tr_file_index_t))); for (NSUInteger index = indexSet.firstIndex, i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index], i++) { files[i] = index; } tr_torrentSetFileDLs(self.fHandle, files, count, state != NSControlStateValueOff); free(files); [self update]; [NSNotificationCenter.defaultCenter postNotificationName:@"TorrentFileCheckChange" object:self]; } - (void)setFilePriority:(tr_priority_t)priority forIndexes:(NSIndexSet*)indexSet { NSUInteger const count = indexSet.count; tr_file_index_t* files = static_cast(tr_malloc(count * sizeof(tr_file_index_t))); for (NSUInteger index = indexSet.firstIndex, i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index], i++) { files[i] = index; } tr_torrentSetFilePriorities(self.fHandle, files, count, priority); tr_free(files); } - (BOOL)hasFilePriority:(tr_priority_t)priority forIndexes:(NSIndexSet*)indexSet { for (NSUInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index]) { if (priority == tr_torrentFile(self.fHandle, index).priority && [self canChangeDownloadCheckForFile:index]) { return YES; } } return NO; } - (NSSet*)filePrioritiesForIndexes:(NSIndexSet*)indexSet { BOOL low = NO, normal = NO, high = NO; NSMutableSet* priorities = [NSMutableSet setWithCapacity:MIN(indexSet.count, 3u)]; for (NSUInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index]) { if (![self canChangeDownloadCheckForFile:index]) { continue; } auto const priority = tr_torrentFile(self.fHandle, index).priority; switch (priority) { case TR_PRI_LOW: if (low) { continue; } low = YES; break; case TR_PRI_NORMAL: if (normal) { continue; } normal = YES; break; case TR_PRI_HIGH: if (high) { continue; } high = YES; break; default: NSAssert2(NO, @"Unknown priority %d for file index %ld", priority, index); } [priorities addObject:@(priority)]; if (low && normal && high) { break; } } return priorities; } - (NSDate*)dateAdded { time_t const date = self.fStat->addedDate; return [NSDate dateWithTimeIntervalSince1970:date]; } - (NSDate*)dateCompleted { time_t const date = self.fStat->doneDate; return date != 0 ? [NSDate dateWithTimeIntervalSince1970:date] : nil; } - (NSDate*)dateActivity { time_t const date = self.fStat->activityDate; return date != 0 ? [NSDate dateWithTimeIntervalSince1970:date] : nil; } - (NSDate*)dateActivityOrAdd { NSDate* date = self.dateActivity; return date ? date : self.dateAdded; } - (NSInteger)secondsDownloading { return self.fStat->secondsDownloading; } - (NSInteger)secondsSeeding { return self.fStat->secondsSeeding; } - (NSInteger)stalledMinutes { if (self.fStat->idleSecs == -1) { return -1; } return self.fStat->idleSecs / 60; } - (BOOL)isStalled { return self.fStat->isStalled; } - (void)updateTimeMachineExclude { [self setTimeMachineExclude:!self.allDownloaded]; } - (NSInteger)stateSortKey { if (!self.active) //paused { if (self.waitingToStart) { return 1; } else { return 0; } } else if (self.seeding) //seeding { return 10; } else //downloading { return 20; } } - (NSString*)trackerSortKey { NSString* best = nil; for (size_t i = 0, n = tr_torrentTrackerCount(self.fHandle); i < n; ++i) { auto const tracker = tr_torrentTracker(self.fHandle, i); NSString* host = @(tracker.host); if (!best || [host localizedCaseInsensitiveCompare:best] == NSOrderedAscending) { best = host; } } return best; } - (tr_torrent*)torrentStruct { return self.fHandle; } - (NSURL*)previewItemURL { NSString* location = self.dataLocation; return location ? [NSURL fileURLWithPath:location] : nil; } #pragma mark - Private - (instancetype)initWithPath:(NSString*)path hash:(NSString*)hashString torrentStruct:(tr_torrent*)torrentStruct magnetAddress:(NSString*)magnetAddress lib:(tr_session*)lib groupValue:(NSNumber*)groupValue removeWhenFinishSeeding:(NSNumber*)removeWhenFinishSeeding downloadFolder:(NSString*)downloadFolder legacyIncompleteFolder:(NSString*)incompleteFolder { if (!(self = [super init])) { return nil; } _fDefaults = NSUserDefaults.standardUserDefaults; if (torrentStruct) { _fHandle = torrentStruct; } else { //set libtransmission settings for initialization tr_ctor* ctor = tr_ctorNew(lib); tr_ctorSetPaused(ctor, TR_FORCE, YES); if (downloadFolder) { tr_ctorSetDownloadDir(ctor, TR_FORCE, downloadFolder.UTF8String); } if (incompleteFolder) { tr_ctorSetIncompleteDir(ctor, incompleteFolder.UTF8String); } bool loaded = false; if (path) { loaded = tr_ctorSetMetainfoFromFile(ctor, path.UTF8String, nullptr); } if (!loaded && magnetAddress) { loaded = tr_ctorSetMetainfoFromMagnetLink(ctor, magnetAddress.UTF8String, nullptr); } if (loaded) { _fHandle = tr_torrentNew(ctor, NULL); } tr_ctorFree(ctor); if (!_fHandle) { return nil; } } tr_torrentSetQueueStartCallback(self.fHandle, startQueueCallback, (__bridge void*)(self)); tr_torrentSetCompletenessCallback(self.fHandle, completenessChangeCallback, (__bridge void*)(self)); tr_torrentSetRatioLimitHitCallback(self.fHandle, ratioLimitHitCallback, (__bridge void*)(self)); tr_torrentSetIdleLimitHitCallback(self.fHandle, idleLimitHitCallback, (__bridge void*)(self)); tr_torrentSetMetadataCallback(self.fHandle, metadataCallback, (__bridge void*)(self)); _fResumeOnWake = NO; //don't do after this point - it messes with auto-group functionality if (!self.magnet) { [self createFileList]; } _fDownloadFolderDetermination = TorrentDeterminationAutomatic; if (groupValue) { _fGroupValueDetermination = TorrentDeterminationUserSpecified; _groupValue = groupValue.intValue; } else { _fGroupValueDetermination = TorrentDeterminationAutomatic; _groupValue = [GroupsController.groups groupIndexForTorrent:self]; } _removeWhenFinishSeeding = removeWhenFinishSeeding ? removeWhenFinishSeeding.boolValue : [_fDefaults boolForKey:@"RemoveWhenFinishSeeding"]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(checkGroupValueForRemoval:) name:@"GroupValueRemoved" object:nil]; _fTimeMachineExcludeInitialized = NO; [self update]; return self; } - (void)createFileList { NSAssert(!self.magnet, @"Cannot create a file list until the torrent is demagnetized"); if (self.folder) { NSInteger const count = self.fileCount; NSMutableArray* flatFileList = [NSMutableArray arrayWithCapacity:count]; FileListNode* tempNode = nil; for (NSInteger i = 0; i < count; i++) { auto const file = tr_torrentFile(self.fHandle, i); NSString* fullPath = @(file.name); NSArray* pathComponents = fullPath.pathComponents; if (!tempNode) { tempNode = [[FileListNode alloc] initWithFolderName:pathComponents[0] path:@"" torrent:self]; } [self insertPathForComponents:pathComponents withComponentIndex:1 forParent:tempNode fileSize:file.length index:i flatList:flatFileList]; } [self sortFileList:tempNode.children]; [self sortFileList:flatFileList]; self.fileList = [[NSArray alloc] initWithArray:tempNode.children]; self.flatFileList = [[NSArray alloc] initWithArray:flatFileList]; } else { FileListNode* node = [[FileListNode alloc] initWithFileName:self.name path:@"" size:self.size index:0 torrent:self]; self.fileList = @[ node ]; self.flatFileList = self.fileList; } } - (void)insertPathForComponents:(NSArray*)components withComponentIndex:(NSUInteger)componentIndex forParent:(FileListNode*)parent fileSize:(uint64_t)size index:(NSInteger)index flatList:(NSMutableArray*)flatFileList { NSParameterAssert(components.count > 0); NSParameterAssert(componentIndex < components.count); NSString* name = components[componentIndex]; BOOL const isFolder = componentIndex < (components.count - 1); //determine if folder node already exists __block FileListNode* node = nil; if (isFolder) { [parent.children enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(FileListNode* searchNode, NSUInteger idx, BOOL* stop) { if ([searchNode.name isEqualToString:name] && searchNode.isFolder) { node = searchNode; *stop = YES; } }]; } //create new folder or file if it doesn't already exist if (!node) { NSString* path = [parent.path stringByAppendingPathComponent:parent.name]; if (isFolder) { node = [[FileListNode alloc] initWithFolderName:name path:path torrent:self]; } else { node = [[FileListNode alloc] initWithFileName:name path:path size:size index:index torrent:self]; [flatFileList addObject:node]; } [parent insertChild:node]; } if (isFolder) { [node insertIndex:index withSize:size]; [self insertPathForComponents:components withComponentIndex:(componentIndex + 1) forParent:node fileSize:size index:index flatList:flatFileList]; } } - (void)sortFileList:(NSMutableArray*)fileNodes { NSSortDescriptor* descriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES selector:@selector(localizedStandardCompare:)]; [fileNodes sortUsingDescriptors:@[ descriptor ]]; [fileNodes enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(FileListNode* node, NSUInteger idx, BOOL* stop) { if (node.isFolder) [self sortFileList:node.children]; }]; } - (void)startQueue { [NSNotificationCenter.defaultCenter postNotificationName:@"UpdateQueue" object:self]; } - (void)completenessChange:(tr_completeness)status wasRunning:(BOOL)wasRunning { self.fStat = tr_torrentStat(self.fHandle); //don't call update yet to avoid auto-stop switch (status) { case TR_SEED: case TR_PARTIAL_SEED: { NSDictionary* statusInfo = @{@"Status" : @(status), @"WasRunning" : @(wasRunning)}; [NSNotificationCenter.defaultCenter postNotificationName:@"TorrentFinishedDownloading" object:self userInfo:statusInfo]; //quarantine the finished data NSString* dataLocation = [self.currentDirectory stringByAppendingPathComponent:self.name]; NSURL* dataLocationUrl = [NSURL fileURLWithPath:dataLocation]; NSDictionary* quarantineProperties = @{ (NSString*)kLSQuarantineTypeKey : (NSString*)kLSQuarantineTypeOtherDownload }; NSError* error = nil; if (![dataLocationUrl setResourceValue:quarantineProperties forKey:NSURLQuarantinePropertiesKey error:&error]) { NSLog(@"Failed to quarantine %@: %@", dataLocation, error.description); } break; } case TR_LEECH: [NSNotificationCenter.defaultCenter postNotificationName:@"TorrentRestartedDownloading" object:self]; break; } [self update]; [self updateTimeMachineExclude]; } - (void)ratioLimitHit { self.fStat = tr_torrentStat(self.fHandle); [NSNotificationCenter.defaultCenter postNotificationName:@"TorrentFinishedSeeding" object:self]; } - (void)idleLimitHit { self.fStat = tr_torrentStat(self.fHandle); [NSNotificationCenter.defaultCenter postNotificationName:@"TorrentFinishedSeeding" object:self]; } - (void)metadataRetrieved { self.fStat = tr_torrentStat(self.fHandle); [self createFileList]; /* If the torrent is in no group, or the group was automatically determined based on criteria evaluated * before we had metadata for this torrent, redetermine the group */ if ((self.fGroupValueDetermination == TorrentDeterminationAutomatic) || (self.groupValue == -1)) { [self setGroupValue:[GroupsController.groups groupIndexForTorrent:self] determinationType:TorrentDeterminationAutomatic]; } //change the location if the group calls for it and it's either not already set or was set automatically before if (((self.fDownloadFolderDetermination == TorrentDeterminationAutomatic) || !tr_torrentGetCurrentDir(self.fHandle)) && [GroupsController.groups usesCustomDownloadLocationForIndex:self.groupValue]) { NSString* location = [GroupsController.groups customDownloadLocationForIndex:self.groupValue]; [self changeDownloadFolderBeforeUsing:location determinationType:TorrentDeterminationAutomatic]; } [NSNotificationCenter.defaultCenter postNotificationName:@"ResetInspector" object:self userInfo:@{ @"Torrent" : self }]; } - (void)renameFinished:(BOOL)success nodes:(NSArray*)nodes completionHandler:(void (^)(BOOL))completionHandler oldPath:(NSString*)oldPath newName:(NSString*)newName { NSParameterAssert(completionHandler != nil); NSParameterAssert(oldPath != nil); NSParameterAssert(newName != nil); NSString* path = oldPath.stringByDeletingLastPathComponent; if (success) { using WeakUpdateNodeAndChildrenForRename = void (^__block __weak)(FileListNode*); using UpdateNodeAndChildrenForRename = void (^)(FileListNode*); NSString* oldName = oldPath.lastPathComponent; WeakUpdateNodeAndChildrenForRename weakUpdateNodeAndChildrenForRename; UpdateNodeAndChildrenForRename updateNodeAndChildrenForRename; weakUpdateNodeAndChildrenForRename = updateNodeAndChildrenForRename = ^(FileListNode* node) { [node updateFromOldName:oldName toNewName:newName inPath:path]; if (node.isFolder) { [node.children enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(FileListNode* childNode, NSUInteger idx, BOOL* stop) { weakUpdateNodeAndChildrenForRename(childNode); }]; } }; if (!nodes) { nodes = self.flatFileList; } [nodes enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(FileListNode* node, NSUInteger idx, BOOL* stop) { updateNodeAndChildrenForRename(node); }]; //resort lists NSMutableArray* fileList = [self.fileList mutableCopy]; [self sortFileList:fileList]; self.fileList = fileList; NSMutableArray* flatFileList = [self.flatFileList mutableCopy]; [self sortFileList:flatFileList]; self.flatFileList = flatFileList; self.fIcon = nil; } else { NSLog(@"Error renaming %@ to %@", oldPath, [path stringByAppendingPathComponent:newName]); } completionHandler(success); } - (BOOL)shouldShowEta { if (self.fStat->activity == TR_STATUS_DOWNLOAD) { return YES; } else if (self.seeding) { //ratio: show if it's set at all if (tr_torrentGetSeedRatio(self.fHandle, NULL)) { return YES; } //idle: show only if remaining time is less than cap if (self.fStat->etaIdle != TR_ETA_NOT_AVAIL && self.fStat->etaIdle < ETA_IDLE_DISPLAY_SEC) { return YES; } } return NO; } - (NSString*)etaString { NSInteger eta; BOOL fromIdle; //don't check for both, since if there's a regular ETA, the torrent isn't idle so it's meaningless if (self.fStat->eta != TR_ETA_NOT_AVAIL && self.fStat->eta != TR_ETA_UNKNOWN) { eta = self.fStat->eta; fromIdle = NO; } else if (self.fStat->etaIdle != TR_ETA_NOT_AVAIL && self.fStat->etaIdle < ETA_IDLE_DISPLAY_SEC) { eta = self.fStat->etaIdle; fromIdle = YES; } else { return NSLocalizedString(@"remaining time unknown", "Torrent -> eta string"); } static NSDateComponentsFormatter* formatter; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ formatter = [NSDateComponentsFormatter new]; formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleShort; formatter.maximumUnitCount = 2; formatter.collapsesLargestUnit = YES; formatter.includesTimeRemainingPhrase = YES; }); NSString* idleString = [formatter stringFromTimeInterval:eta]; if (fromIdle) { idleString = [idleString stringByAppendingFormat:@" (%@)", NSLocalizedString(@"inactive", "Torrent -> eta string")]; } return idleString; } - (void)setTimeMachineExclude:(BOOL)exclude { NSString* path; if ((path = self.dataLocation)) { CSBackupSetItemExcluded((__bridge CFURLRef)[NSURL fileURLWithPath:path], exclude, false); self.fTimeMachineExcludeInitialized = YES; } } @end