1/****************************************************************************** 2 * Copyright (c) 2006-2012 Transmission authors and contributors 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a 5 * copy of this software and associated documentation files (the "Software"), 6 * to deal in the Software without restriction, including without limitation 7 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 * and/or sell copies of the Software, and to permit persons to whom the 9 * Software is furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 * DEALINGS IN THE SOFTWARE. 21 *****************************************************************************/ 22 23#include <libtransmission/transmission.h> 24#include <libtransmission/error.h> 25#include <libtransmission/log.h> 26#include <libtransmission/utils.h> // tr_new() 27 28#import "Torrent.h" 29#import "GroupsController.h" 30#import "FileListNode.h" 31#import "NSApplicationAdditions.h" 32#import "NSStringAdditions.h" 33#import "TrackerNode.h" 34 35#define ETA_IDLE_DISPLAY_SEC (2*60) 36 37@interface Torrent (Private) 38 39- (id) initWithPath: (NSString *) path hash: (NSString *) hashString torrentStruct: (tr_torrent *) torrentStruct 40 magnetAddress: (NSString *) magnetAddress lib: (tr_session *) lib 41 groupValue: (NSNumber *) groupValue 42 removeWhenFinishSeeding: (NSNumber *) removeWhenFinishSeeding 43 downloadFolder: (NSString *) downloadFolder 44 legacyIncompleteFolder: (NSString *) incompleteFolder; 45 46- (void) createFileList; 47- (void) insertPathForComponents: (NSArray *) components withComponentIndex: (NSUInteger) componentIndex forParent: (FileListNode *) parent fileSize: (uint64_t) size 48 index: (NSInteger) index flatList: (NSMutableArray *) flatFileList; 49- (void) sortFileList: (NSMutableArray *) fileNodes; 50 51- (void) startQueue; 52- (void) completenessChange: (tr_completeness) status wasRunning: (BOOL) wasRunning; 53- (void) ratioLimitHit; 54- (void) idleLimitHit; 55- (void) metadataRetrieved; 56- (void)renameFinished: (BOOL) success nodes: (NSArray *) nodes completionHandler: (void (^)(BOOL)) completionHandler oldPath: (NSString *) oldPath newName: (NSString *) newName; 57 58- (BOOL) shouldShowEta; 59- (NSString *) etaString; 60 61- (void) setTimeMachineExclude: (BOOL) exclude; 62 63@end 64 65void startQueueCallback(tr_torrent * torrent, void * torrentData) 66{ 67 dispatch_async(dispatch_get_main_queue(), ^{ 68 [(__bridge Torrent *)torrentData startQueue]; 69 }); 70} 71 72void completenessChangeCallback(tr_torrent * torrent, tr_completeness status, bool wasRunning, void * torrentData) 73{ 74 dispatch_async(dispatch_get_main_queue(), ^{ 75 [(__bridge Torrent *)torrentData completenessChange: status wasRunning: wasRunning]; 76 }); 77} 78 79void ratioLimitHitCallback(tr_torrent * torrent, void * torrentData) 80{ 81 dispatch_async(dispatch_get_main_queue(), ^{ 82 [(__bridge Torrent *)torrentData ratioLimitHit]; 83 }); 84} 85 86void idleLimitHitCallback(tr_torrent * torrent, void * torrentData) 87{ 88 dispatch_async(dispatch_get_main_queue(), ^{ 89 [(__bridge Torrent *)torrentData idleLimitHit]; 90 }); 91} 92 93void metadataCallback(tr_torrent * torrent, void * torrentData) 94{ 95 dispatch_async(dispatch_get_main_queue(), ^{ 96 [(__bridge Torrent *)torrentData metadataRetrieved]; 97 }); 98} 99 100void renameCallback(tr_torrent * torrent, const char * oldPathCharString, const char * newNameCharString, int error, void * contextInfo) 101{ 102 @autoreleasepool { 103 NSString * oldPath = @(oldPathCharString); 104 NSString * newName = @(newNameCharString); 105 106 dispatch_async(dispatch_get_main_queue(), ^{ 107 NSDictionary * contextDict = (__bridge_transfer NSDictionary *)contextInfo; 108 Torrent * torrentObject = contextDict[@"Torrent"]; 109 [torrentObject renameFinished: error == 0 nodes: contextDict[@"Nodes"] completionHandler: contextDict[@"CompletionHandler"] oldPath: oldPath newName: newName]; 110 }); 111 } 112} 113 114bool trashDataFile(const char * filename, tr_error ** error) 115{ 116 if (filename == NULL) 117 return false; 118 119 @autoreleasepool 120 { 121 NSError * localError; 122 if (![Torrent trashFile: @(filename) error: &localError]) 123 { 124 tr_error_set_literal(error, [localError code], [[localError description] UTF8String]); 125 return false; 126 } 127 } 128 129 return true; 130} 131 132@implementation Torrent 133 134#warning remove ivars in header when 64-bit only (or it compiles in 32-bit mode) 135@synthesize removeWhenFinishSeeding = fRemoveWhenFinishSeeding; 136 137- (id) initWithPath: (NSString *) path location: (NSString *) location deleteTorrentFile: (BOOL) torrentDelete 138 lib: (tr_session *) lib 139{ 140 self = [self initWithPath: path hash: nil torrentStruct: NULL magnetAddress: nil lib: lib 141 groupValue: nil 142 removeWhenFinishSeeding: nil 143 downloadFolder: location 144 legacyIncompleteFolder: nil]; 145 146 if (self) 147 { 148 if (torrentDelete && ![[self torrentLocation] isEqualToString: path]) 149 [Torrent trashFile: path error: nil]; 150 } 151 return self; 152} 153 154- (id) initWithTorrentStruct: (tr_torrent *) torrentStruct location: (NSString *) location lib: (tr_session *) lib 155{ 156 self = [self initWithPath: nil hash: nil torrentStruct: torrentStruct magnetAddress: nil lib: lib 157 groupValue: nil 158 removeWhenFinishSeeding: nil 159 downloadFolder: location 160 legacyIncompleteFolder: nil]; 161 162 return self; 163} 164 165- (id) initWithMagnetAddress: (NSString *) address location: (NSString *) location lib: (tr_session *) lib 166{ 167 self = [self initWithPath: nil hash: nil torrentStruct: nil magnetAddress: address 168 lib: lib groupValue: nil 169 removeWhenFinishSeeding: nil 170 downloadFolder: location legacyIncompleteFolder: nil]; 171 172 return self; 173} 174 175- (id) initWithHistory: (NSDictionary *) history lib: (tr_session *) lib forcePause: (BOOL) pause 176{ 177 self = [self initWithPath: history[@"InternalTorrentPath"] 178 hash: history[@"TorrentHash"] 179 torrentStruct: NULL 180 magnetAddress: nil 181 lib: lib 182 groupValue: history[@"GroupValue"] 183 removeWhenFinishSeeding: history[@"RemoveWhenFinishSeeding"] 184 downloadFolder: history[@"DownloadFolder"] //upgrading from versions < 1.80 185 legacyIncompleteFolder: [history[@"UseIncompleteFolder"] boolValue] //upgrading from versions < 1.80 186 ? history[@"IncompleteFolder"] : nil]; 187 188 if (self) 189 { 190 //start transfer 191 NSNumber * active; 192 if (!pause && (active = history[@"Active"]) && [active boolValue]) 193 { 194 fStat = tr_torrentStat(fHandle); 195 [self startTransferNoQueue]; 196 } 197 198 //upgrading from versions < 1.30: get old added, activity, and done dates 199 NSDate * date; 200 if ((date = history[@"Date"])) 201 tr_torrentSetAddedDate(fHandle, [date timeIntervalSince1970]); 202 if ((date = history[@"DateActivity"])) 203 tr_torrentSetActivityDate(fHandle, [date timeIntervalSince1970]); 204 if ((date = history[@"DateCompleted"])) 205 tr_torrentSetDoneDate(fHandle, [date timeIntervalSince1970]); 206 207 //upgrading from versions < 1.60: get old stop ratio settings 208 NSNumber * ratioSetting; 209 if ((ratioSetting = history[@"RatioSetting"])) 210 { 211 switch ([ratioSetting intValue]) 212 { 213 case NSOnState: [self setRatioSetting: TR_RATIOLIMIT_SINGLE]; break; 214 case NSOffState: [self setRatioSetting: TR_RATIOLIMIT_UNLIMITED]; break; 215 case NSMixedState: [self setRatioSetting: TR_RATIOLIMIT_GLOBAL]; break; 216 } 217 } 218 NSNumber * ratioLimit; 219 if ((ratioLimit = history[@"RatioLimit"])) 220 [self setRatioLimit: [ratioLimit floatValue]]; 221 } 222 return self; 223} 224 225- (NSDictionary *) history 226{ 227 return @{ 228 @"InternalTorrentPath": [self torrentLocation], 229 @"TorrentHash": [self hashString], 230 @"Active": @([self isActive]), 231 @"WaitToStart": @([self waitingToStart]), 232 @"GroupValue": @(fGroupValue), 233 @"RemoveWhenFinishSeeding": @(fRemoveWhenFinishSeeding)}; 234} 235 236- (void) dealloc 237{ 238 [[NSNotificationCenter defaultCenter] removeObserver: self]; 239 240 if (fFileStat) 241 tr_torrentFilesFree(fFileStat, [self fileCount]); 242} 243 244- (NSString *) description 245{ 246 return [@"Torrent: " stringByAppendingString: [self name]]; 247} 248 249- (id) copyWithZone: (NSZone *) zone 250{ 251 return self; 252} 253 254- (void) closeRemoveTorrent: (BOOL) trashFiles 255{ 256 //allow the file to be indexed by Time Machine 257 [self setTimeMachineExclude: NO]; 258 259 tr_torrentRemove(fHandle, trashFiles, trashDataFile); 260} 261 262- (void) changeDownloadFolderBeforeUsing: (NSString *) folder determinationType: (TorrentDeterminationType) determinationType 263{ 264 //if data existed in original download location, unexclude it before changing the location 265 [self setTimeMachineExclude: NO]; 266 267 tr_torrentSetDownloadDir(fHandle, [folder UTF8String]); 268 269 fDownloadFolderDetermination = determinationType; 270} 271 272- (NSString *) currentDirectory 273{ 274 return @(tr_torrentGetCurrentDir(fHandle)); 275} 276 277- (void) getAvailability: (int8_t *) tab size: (NSInteger) size 278{ 279 tr_torrentAvailability(fHandle, tab, size); 280} 281 282- (void) getAmountFinished: (float *) tab size: (NSInteger) size 283{ 284 tr_torrentAmountFinished(fHandle, tab, size); 285} 286 287- (NSIndexSet *) previousFinishedPieces 288{ 289 //if the torrent hasn't been seen in a bit, and therefore hasn't been refreshed, return nil 290 if (fPreviousFinishedIndexesDate && [fPreviousFinishedIndexesDate timeIntervalSinceNow] > -2.0) 291 return fPreviousFinishedIndexes; 292 else 293 return nil; 294} 295 296- (void) setPreviousFinishedPieces: (NSIndexSet *) indexes 297{ 298 fPreviousFinishedIndexes = indexes; 299 300 fPreviousFinishedIndexesDate = indexes != nil ? [[NSDate alloc] init] : nil; 301} 302 303- (void) update 304{ 305 //get previous stalled value before update 306 const BOOL wasStalled = fStat != NULL && [self isStalled]; 307 308 fStat = tr_torrentStat(fHandle); 309 310 //make sure the "active" filter is updated when stalled-ness changes 311 if (wasStalled != [self isStalled]) 312 //posting asynchronously with coalescing to prevent stack overflow on lots of torrents changing state at the same time 313 [[NSNotificationQueue defaultQueue] enqueueNotification: [NSNotification notificationWithName: @"UpdateQueue" object: self] 314 postingStyle: NSPostASAP 315 coalesceMask: NSNotificationCoalescingOnName 316 forModes: nil]; 317 318 //when the torrent is first loaded, update the time machine exclusion 319 if (!fTimeMachineExcludeInitialized) 320 [self updateTimeMachineExclude]; 321} 322 323- (void) startTransferIgnoringQueue: (BOOL) ignoreQueue 324{ 325 if ([self alertForRemainingDiskSpace]) 326 { 327 ignoreQueue ? tr_torrentStartNow(fHandle) : tr_torrentStart(fHandle); 328 [self update]; 329 330 //capture, specifically, stop-seeding settings changing to unlimited 331 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil]; 332 } 333} 334 335- (void) startTransferNoQueue 336{ 337 [self startTransferIgnoringQueue: YES]; 338} 339 340- (void) startTransfer 341{ 342 [self startTransferIgnoringQueue: NO]; 343} 344 345- (void) stopTransfer 346{ 347 tr_torrentStop(fHandle); 348 [self update]; 349} 350 351- (void) sleep 352{ 353 if ((fResumeOnWake = [self isActive])) 354 tr_torrentStop(fHandle); 355} 356 357- (void) wakeUp 358{ 359 if (fResumeOnWake) 360 { 361 tr_logAddNamedInfo( fInfo->name, "restarting because of wakeUp" ); 362 tr_torrentStart(fHandle); 363 } 364} 365 366- (NSInteger) queuePosition 367{ 368 return fStat->queuePosition; 369} 370 371- (void) setQueuePosition: (NSUInteger) index 372{ 373 tr_torrentSetQueuePosition(fHandle, index); 374} 375 376- (void) manualAnnounce 377{ 378 tr_torrentManualUpdate(fHandle); 379} 380 381- (BOOL) canManualAnnounce 382{ 383 return tr_torrentCanManualUpdate(fHandle); 384} 385 386- (void) resetCache 387{ 388 tr_torrentVerify(fHandle, NULL, NULL); 389 [self update]; 390} 391 392- (BOOL) isMagnet 393{ 394 return !tr_torrentHasMetadata(fHandle); 395} 396 397- (NSString *) magnetLink 398{ 399 return @(tr_torrentGetMagnetLink(fHandle)); 400} 401 402- (CGFloat) ratio 403{ 404 return fStat->ratio; 405} 406 407- (tr_ratiolimit) ratioSetting 408{ 409 return tr_torrentGetRatioMode(fHandle); 410} 411 412- (void) setRatioSetting: (tr_ratiolimit) setting 413{ 414 tr_torrentSetRatioMode(fHandle, setting); 415} 416 417- (CGFloat) ratioLimit 418{ 419 return tr_torrentGetRatioLimit(fHandle); 420} 421 422- (void) setRatioLimit: (CGFloat) limit 423{ 424 NSParameterAssert(limit >= 0); 425 426 tr_torrentSetRatioLimit(fHandle, limit); 427} 428 429- (CGFloat) progressStopRatio 430{ 431 return fStat->seedRatioPercentDone; 432} 433 434- (tr_idlelimit) idleSetting 435{ 436 return tr_torrentGetIdleMode(fHandle); 437} 438 439- (void) setIdleSetting: (tr_idlelimit) setting 440{ 441 tr_torrentSetIdleMode(fHandle, setting); 442} 443 444- (NSUInteger) idleLimitMinutes 445{ 446 return tr_torrentGetIdleLimit(fHandle); 447} 448 449- (void) setIdleLimitMinutes: (NSUInteger) limit 450{ 451 NSParameterAssert(limit > 0); 452 453 tr_torrentSetIdleLimit(fHandle, limit); 454} 455 456- (BOOL) usesSpeedLimit: (BOOL) upload 457{ 458 return tr_torrentUsesSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN); 459} 460 461- (void) setUseSpeedLimit: (BOOL) use upload: (BOOL) upload 462{ 463 tr_torrentUseSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN, use); 464} 465 466- (NSInteger) speedLimit: (BOOL) upload 467{ 468 return tr_torrentGetSpeedLimit_KBps(fHandle, upload ? TR_UP : TR_DOWN); 469} 470 471- (void) setSpeedLimit: (NSInteger) limit upload: (BOOL) upload 472{ 473 tr_torrentSetSpeedLimit_KBps(fHandle, upload ? TR_UP : TR_DOWN, limit); 474} 475 476- (BOOL) usesGlobalSpeedLimit 477{ 478 return tr_torrentUsesSessionLimits(fHandle); 479} 480 481- (void) setUseGlobalSpeedLimit: (BOOL) use 482{ 483 tr_torrentUseSessionLimits(fHandle, use); 484} 485 486- (void) setMaxPeerConnect: (uint16_t) count 487{ 488 NSParameterAssert(count > 0); 489 490 tr_torrentSetPeerLimit(fHandle, count); 491} 492 493- (uint16_t) maxPeerConnect 494{ 495 return tr_torrentGetPeerLimit(fHandle); 496} 497- (BOOL) waitingToStart 498{ 499 return fStat->activity == TR_STATUS_DOWNLOAD_WAIT || fStat->activity == TR_STATUS_SEED_WAIT; 500} 501 502- (tr_priority_t) priority 503{ 504 return tr_torrentGetPriority(fHandle); 505} 506 507- (void) setPriority: (tr_priority_t) priority 508{ 509 return tr_torrentSetPriority(fHandle, priority); 510} 511 512+ (BOOL) trashFile: (NSString *) path error: (NSError **) error 513{ 514 //attempt to move to trash 515 if (![[NSWorkspace sharedWorkspace] performFileOperation: NSWorkspaceRecycleOperation 516 source: [path stringByDeletingLastPathComponent] destination: @"" 517 files: @[[path lastPathComponent]] tag: nil]) 518 { 519 //if cannot trash, just delete it (will work if it's on a remote volume) 520 NSError * localError; 521 if (![[NSFileManager defaultManager] removeItemAtPath: path error: &localError]) 522 { 523 NSLog(@"old Could not trash %@: %@", path, [localError localizedDescription]); 524 if (error != nil) 525 *error = localError; 526 return NO; 527 } 528 else 529 { 530 NSLog(@"old removed %@", path); 531 } 532 } 533 534 return YES; 535} 536 537- (void) moveTorrentDataFileTo: (NSString *) folder 538{ 539 NSString * oldFolder = [self currentDirectory]; 540 if ([oldFolder isEqualToString: folder]) 541 return; 542 543 //check if moving inside itself 544 NSArray * oldComponents = [oldFolder pathComponents], 545 * newComponents = [folder pathComponents]; 546 const NSUInteger oldCount = [oldComponents count]; 547 548 if (oldCount < [newComponents count] && [newComponents[oldCount] isEqualToString: [self name]] 549 && [folder hasPrefix: oldFolder]) 550 { 551 NSAlert * alert = [[NSAlert alloc] init]; 552 [alert setMessageText: NSLocalizedString(@"A folder cannot be moved to inside itself.", 553 "Move inside itself alert -> title")]; 554 [alert setInformativeText: [NSString stringWithFormat: 555 NSLocalizedString(@"The move operation of \"%@\" cannot be done.", 556 "Move inside itself alert -> message"), [self name]]]; 557 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move inside itself alert -> button")]; 558 559 [alert runModal]; 560 561 return; 562 } 563 564 volatile int status; 565 tr_torrentSetLocation(fHandle, [folder UTF8String], YES, NULL, &status); 566 567 while (status == TR_LOC_MOVING) //block while moving (for now) 568 [NSThread sleepForTimeInterval: 0.05]; 569 570 if (status == TR_LOC_DONE) 571 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateStats" object: nil]; 572 else 573 { 574 NSAlert * alert = [[NSAlert alloc] init]; 575 [alert setMessageText: NSLocalizedString(@"There was an error moving the data file.", "Move error alert -> title")]; 576 [alert setInformativeText: [NSString stringWithFormat: 577 NSLocalizedString(@"The move operation of \"%@\" cannot be done.", "Move error alert -> message"), [self name]]]; 578 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move error alert -> button")]; 579 580 [alert runModal]; 581 } 582 583 [self updateTimeMachineExclude]; 584} 585 586- (void) copyTorrentFileTo: (NSString *) path 587{ 588 [[NSFileManager defaultManager] copyItemAtPath: [self torrentLocation] toPath: path error: NULL]; 589} 590 591- (BOOL) alertForRemainingDiskSpace 592{ 593 if ([self allDownloaded] || ![fDefaults boolForKey: @"WarningRemainingSpace"]) 594 return YES; 595 596 NSString * downloadFolder = [self currentDirectory]; 597 NSDictionary * systemAttributes; 598 if ((systemAttributes = [[NSFileManager defaultManager] attributesOfFileSystemForPath: downloadFolder error: NULL])) 599 { 600 const uint64_t remainingSpace = [systemAttributes[NSFileSystemFreeSize] unsignedLongLongValue]; 601 602 //if the remaining space is greater than the size left, then there is enough space regardless of preallocation 603 if (remainingSpace < [self sizeLeft] && remainingSpace < tr_torrentGetBytesLeftToAllocate(fHandle)) 604 { 605 NSString * volumeName = [[NSFileManager defaultManager] componentsToDisplayForPath: downloadFolder][0]; 606 607 NSAlert * alert = [[NSAlert alloc] init]; 608 [alert setMessageText: [NSString stringWithFormat: 609 NSLocalizedString(@"Not enough remaining disk space to download \"%@\" completely.", 610 "Torrent disk space alert -> title"), [self name]]]; 611 [alert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"The transfer will be paused." 612 " Clear up space on %@ or deselect files in the torrent inspector to continue.", 613 "Torrent disk space alert -> message"), volumeName]]; 614 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Torrent disk space alert -> button")]; 615 [alert addButtonWithTitle: NSLocalizedString(@"Download Anyway", "Torrent disk space alert -> button")]; 616 617 [alert setShowsSuppressionButton: YES]; 618 [[alert suppressionButton] setTitle: NSLocalizedString(@"Do not check disk space again", 619 "Torrent disk space alert -> button")]; 620 621 const NSInteger result = [alert runModal]; 622 if ([[alert suppressionButton] state] == NSOnState) 623 [fDefaults setBool: NO forKey: @"WarningRemainingSpace"]; 624 625 return result != NSAlertFirstButtonReturn; 626 } 627 } 628 return YES; 629} 630 631- (NSImage *) icon 632{ 633 if ([self isMagnet]) 634 return [NSImage imageNamed: @"Magnet"]; 635 636 if (!fIcon) 637 fIcon = [self isFolder] ? [NSImage imageNamed: NSImageNameFolder] 638 : [[NSWorkspace sharedWorkspace] iconForFileType: [[self name] pathExtension]]; 639 return fIcon; 640} 641 642- (NSString *) name 643{ 644 return fInfo->name != NULL ? @(fInfo->name) : fHashString; 645} 646 647- (BOOL) isFolder 648{ 649 return fInfo->isFolder; 650} 651 652- (uint64_t) size 653{ 654 return fInfo->totalSize; 655} 656 657- (uint64_t) sizeLeft 658{ 659 return fStat->leftUntilDone; 660} 661 662- (NSMutableArray *) allTrackerStats 663{ 664 int count; 665 tr_tracker_stat * stats = tr_torrentTrackers(fHandle, &count); 666 667 NSMutableArray * trackers = [NSMutableArray arrayWithCapacity: (count > 0 ? count + (stats[count-1].tier + 1) : 0)]; 668 669 int prevTier = -1; 670 for (int i=0; i < count; ++i) 671 { 672 if (stats[i].tier != prevTier) 673 { 674 [trackers addObject: @{ @"Tier" : @(stats[i].tier + 1), @"Name" : [self name] }]; 675 prevTier = stats[i].tier; 676 } 677 678 TrackerNode * tracker = [[TrackerNode alloc] initWithTrackerStat: &stats[i] torrent: self]; 679 [trackers addObject: tracker]; 680 } 681 682 tr_torrentTrackersFree(stats, count); 683 return trackers; 684} 685 686- (NSArray *) allTrackersFlat 687{ 688 NSMutableArray * allTrackers = [NSMutableArray arrayWithCapacity: fInfo->trackerCount]; 689 690 for (NSInteger i=0; i < fInfo->trackerCount; i++) 691 [allTrackers addObject: @(fInfo->trackers[i].announce)]; 692 693 return allTrackers; 694} 695 696- (BOOL) addTrackerToNewTier: (NSString *) tracker 697{ 698 tracker = [tracker stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; 699 700 if ([tracker rangeOfString: @"://"].location == NSNotFound) 701 tracker = [@"http://" stringByAppendingString: tracker]; 702 703 //recreate the tracker structure 704 const int oldTrackerCount = fInfo->trackerCount; 705 tr_tracker_info * trackerStructs = tr_new(tr_tracker_info, oldTrackerCount+1); 706 for (int i = 0; i < oldTrackerCount; ++i) 707 trackerStructs[i] = fInfo->trackers[i]; 708 709 trackerStructs[oldTrackerCount].announce = (char *)[tracker UTF8String]; 710 trackerStructs[oldTrackerCount].tier = trackerStructs[oldTrackerCount-1].tier + 1; 711 trackerStructs[oldTrackerCount].id = oldTrackerCount; 712 713 const BOOL success = tr_torrentSetAnnounceList(fHandle, trackerStructs, oldTrackerCount+1); 714 tr_free(trackerStructs); 715 716 return success; 717} 718 719- (void) removeTrackers: (NSSet *) trackers 720{ 721 //recreate the tracker structure 722 tr_tracker_info * trackerStructs = tr_new(tr_tracker_info, fInfo->trackerCount); 723 724 NSUInteger newCount = 0; 725 for (NSUInteger i = 0; i < fInfo->trackerCount; i++) 726 { 727 if (![trackers containsObject: @(fInfo->trackers[i].announce)]) 728 trackerStructs[newCount++] = fInfo->trackers[i]; 729 } 730 731 const BOOL success = tr_torrentSetAnnounceList(fHandle, trackerStructs, newCount); 732 NSAssert(success, @"Removing tracker addresses failed"); 733 734 tr_free(trackerStructs); 735} 736 737- (NSString *) comment 738{ 739 return fInfo->comment ? @(fInfo->comment) : @""; 740} 741 742- (NSString *) creator 743{ 744 return fInfo->creator ? @(fInfo->creator) : @""; 745} 746 747- (NSDate *) dateCreated 748{ 749 NSInteger date = fInfo->dateCreated; 750 return date > 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil; 751} 752 753- (NSInteger) pieceSize 754{ 755 return fInfo->pieceSize; 756} 757 758- (NSInteger) pieceCount 759{ 760 return fInfo->pieceCount; 761} 762 763- (NSString *) hashString 764{ 765 return fHashString; 766} 767 768- (BOOL) privateTorrent 769{ 770 return fInfo->isPrivate; 771} 772 773- (NSString *) torrentLocation 774{ 775 return fInfo->torrent ? @(fInfo->torrent) : @""; 776} 777 778- (NSString *) dataLocation 779{ 780 if ([self isMagnet]) 781 return nil; 782 783 if ([self isFolder]) 784 { 785 NSString * dataLocation = [[self currentDirectory] stringByAppendingPathComponent: [self name]]; 786 787 if (![[NSFileManager defaultManager] fileExistsAtPath: dataLocation]) 788 return nil; 789 790 return dataLocation; 791 } 792 else 793 { 794 char * location = tr_torrentFindFile(fHandle, 0); 795 if (location == NULL) 796 return nil; 797 798 NSString * dataLocation = @(location); 799 free(location); 800 801 return dataLocation; 802 } 803} 804 805- (NSString *) fileLocation: (FileListNode *) node 806{ 807 if ([node isFolder]) 808 { 809 NSString * basePath = [[node path] stringByAppendingPathComponent: [node name]]; 810 NSString * dataLocation = [[self currentDirectory] stringByAppendingPathComponent: basePath]; 811 812 if (![[NSFileManager defaultManager] fileExistsAtPath: dataLocation]) 813 return nil; 814 815 return dataLocation; 816 } 817 else 818 { 819 char * location = tr_torrentFindFile(fHandle, [[node indexes] firstIndex]); 820 if (location == NULL) 821 return nil; 822 823 NSString * dataLocation = @(location); 824 free(location); 825 826 return dataLocation; 827 } 828} 829 830- (void) renameTorrent: (NSString *) newName completionHandler: (void (^)(BOOL didRename)) completionHandler 831{ 832 NSParameterAssert(newName != nil); 833 NSParameterAssert(![newName isEqualToString: @""]); 834 835 NSDictionary * contextInfo = @{ @"Torrent" : self, @"CompletionHandler" : [completionHandler copy] }; 836 837 tr_torrentRenamePath(fHandle, fInfo->name, [newName UTF8String], renameCallback, (__bridge_retained void *)(contextInfo)); 838} 839 840- (void) renameFileNode: (FileListNode *) node withName: (NSString *) newName completionHandler: (void (^)(BOOL didRename)) completionHandler 841{ 842 NSParameterAssert([node torrent] == self); 843 NSParameterAssert(newName != nil); 844 NSParameterAssert(![newName isEqualToString: @""]); 845 846 NSDictionary * contextInfo = @{ @"Torrent" : self, @"Nodes" : @[ node ], @"CompletionHandler" : [completionHandler copy] }; 847 848 NSString * oldPath = [[node path] stringByAppendingPathComponent: [node name]]; 849 tr_torrentRenamePath(fHandle, [oldPath UTF8String], [newName UTF8String], renameCallback, (__bridge_retained void *)(contextInfo)); 850} 851 852- (CGFloat) progress 853{ 854 return fStat->percentComplete; 855} 856 857- (CGFloat) progressDone 858{ 859 return fStat->percentDone; 860} 861 862- (CGFloat) progressLeft 863{ 864 if ([self size] == 0) //magnet links 865 return 0.0; 866 867 return (CGFloat)[self sizeLeft] / [self size]; 868} 869 870- (CGFloat) checkingProgress 871{ 872 return fStat->recheckProgress; 873} 874 875- (CGFloat) availableDesired 876{ 877 return (CGFloat)fStat->desiredAvailable / [self sizeLeft]; 878} 879 880- (BOOL) isActive 881{ 882 return fStat->activity != TR_STATUS_STOPPED && fStat->activity != TR_STATUS_DOWNLOAD_WAIT && fStat->activity != TR_STATUS_SEED_WAIT; 883} 884 885- (BOOL) isSeeding 886{ 887 return fStat->activity == TR_STATUS_SEED; 888} 889 890- (BOOL) isChecking 891{ 892 return fStat->activity == TR_STATUS_CHECK || fStat->activity == TR_STATUS_CHECK_WAIT; 893} 894 895- (BOOL) isCheckingWaiting 896{ 897 return fStat->activity == TR_STATUS_CHECK_WAIT; 898} 899 900- (BOOL) allDownloaded 901{ 902 return [self sizeLeft] == 0 && ![self isMagnet]; 903} 904 905- (BOOL) isComplete 906{ 907 return [self progress] >= 1.0; 908} 909 910- (BOOL) isFinishedSeeding 911{ 912 return fStat->finished; 913} 914 915- (BOOL) isError 916{ 917 return fStat->error == TR_STAT_LOCAL_ERROR; 918} 919 920- (BOOL) isAnyErrorOrWarning 921{ 922 return fStat->error != TR_STAT_OK; 923} 924 925- (NSString *) errorMessage 926{ 927 if (![self isAnyErrorOrWarning]) 928 return @""; 929 930 NSString * error; 931 if (!(error = @(fStat->errorString)) 932 && !(error = [NSString stringWithCString: fStat->errorString encoding: NSISOLatin1StringEncoding])) 933 error = [NSString stringWithFormat: @"(%@)", NSLocalizedString(@"unreadable error", "Torrent -> error string unreadable")]; 934 935 //libtransmission uses "Set Location", Mac client uses "Move data file to..." - very hacky! 936 error = [error stringByReplacingOccurrencesOfString: @"Set Location" withString: [@"Move Data File To" stringByAppendingEllipsis]]; 937 938 return error; 939} 940 941- (NSArray *) peers 942{ 943 int totalPeers; 944 tr_peer_stat * peers = tr_torrentPeers(fHandle, &totalPeers); 945 946 NSMutableArray * peerDicts = [NSMutableArray arrayWithCapacity: totalPeers]; 947 948 for (int i = 0; i < totalPeers; i++) 949 { 950 tr_peer_stat * peer = &peers[i]; 951 NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity: 12]; 952 953 dict[@"Name"] = [self name]; 954 dict[@"From"] = @(peer->from); 955 dict[@"IP"] = @(peer->addr); 956 dict[@"Port"] = @(peer->port); 957 dict[@"Progress"] = @(peer->progress); 958 dict[@"Seed"] = @(peer->isSeed); 959 dict[@"Encryption"] = @(peer->isEncrypted); 960 dict[@"uTP"] = @(peer->isUTP); 961 dict[@"Client"] = @(peer->client); 962 dict[@"Flags"] = @(peer->flagStr); 963 964 if (peer->isUploadingTo) 965 dict[@"UL To Rate"] = @(peer->rateToPeer_KBps); 966 if (peer->isDownloadingFrom) 967 dict[@"DL From Rate"] = @(peer->rateToClient_KBps); 968 969 [peerDicts addObject: dict]; 970 } 971 972 tr_torrentPeersFree(peers, totalPeers); 973 974 return peerDicts; 975} 976 977- (NSUInteger) webSeedCount 978{ 979 return fInfo->webseedCount; 980} 981 982- (NSArray *) webSeeds 983{ 984 NSMutableArray * webSeeds = [NSMutableArray arrayWithCapacity: fInfo->webseedCount]; 985 986 double * dlSpeeds = tr_torrentWebSpeeds_KBps(fHandle); 987 988 for (NSInteger i = 0; i < fInfo->webseedCount; i++) 989 { 990 NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity: 3]; 991 992 dict[@"Name"] = [self name]; 993 dict[@"Address"] = @(fInfo->webseeds[i]); 994 995 if (dlSpeeds[i] != -1.0) 996 dict[@"DL From Rate"] = @(dlSpeeds[i]); 997 998 [webSeeds addObject: dict]; 999 } 1000 1001 tr_free(dlSpeeds); 1002 1003 return webSeeds; 1004} 1005 1006- (NSString *) progressString 1007{ 1008 if ([self isMagnet]) 1009 { 1010 NSString * progressString = fStat->metadataPercentComplete > 0.0 1011 ? [NSString stringWithFormat: NSLocalizedString(@"%@ of torrent metadata retrieved", 1012 "Torrent -> progress string"), [NSString percentString: fStat->metadataPercentComplete longDecimals: YES]] 1013 : NSLocalizedString(@"torrent metadata needed", "Torrent -> progress string"); 1014 1015 return [NSString stringWithFormat: @"%@ - %@", NSLocalizedString(@"Magnetized transfer", "Torrent -> progress string"), 1016 progressString]; 1017 } 1018 1019 NSString * string; 1020 1021 if (![self allDownloaded]) 1022 { 1023 CGFloat progress; 1024 if ([self isFolder] && [fDefaults boolForKey: @"DisplayStatusProgressSelected"]) 1025 { 1026 string = [NSString stringForFilePartialSize: [self haveTotal] fullSize: [self totalSizeSelected]]; 1027 progress = [self progressDone]; 1028 } 1029 else 1030 { 1031 string = [NSString stringForFilePartialSize: [self haveTotal] fullSize: [self size]]; 1032 progress = [self progress]; 1033 } 1034 1035 string = [string stringByAppendingFormat: @" (%@)", [NSString percentString: progress longDecimals: YES]]; 1036 } 1037 else 1038 { 1039 NSString * downloadString; 1040 if (![self isComplete]) //only multifile possible 1041 { 1042 if ([fDefaults boolForKey: @"DisplayStatusProgressSelected"]) 1043 downloadString = [NSString stringWithFormat: NSLocalizedString(@"%@ selected", "Torrent -> progress string"), 1044 [NSString stringForFileSize: [self haveTotal]]]; 1045 else 1046 { 1047 downloadString = [NSString stringForFilePartialSize: [self haveTotal] fullSize: [self size]]; 1048 downloadString = [downloadString stringByAppendingFormat: @" (%@)", 1049 [NSString percentString: [self progress] longDecimals: YES]]; 1050 } 1051 } 1052 else 1053 downloadString = [NSString stringForFileSize: [self size]]; 1054 1055 NSString * uploadString = [NSString stringWithFormat: NSLocalizedString(@"uploaded %@ (Ratio: %@)", 1056 "Torrent -> progress string"), [NSString stringForFileSize: [self uploadedTotal]], 1057 [NSString stringForRatio: [self ratio]]]; 1058 1059 string = [downloadString stringByAppendingFormat: @", %@", uploadString]; 1060 } 1061 1062 //add time when downloading or seed limit set 1063 if ([self shouldShowEta]) 1064 string = [string stringByAppendingFormat: @" - %@", [self etaString]]; 1065 1066 return string; 1067} 1068 1069- (NSString *) statusString 1070{ 1071 NSString * string; 1072 1073 if ([self isAnyErrorOrWarning]) 1074 { 1075 switch (fStat->error) 1076 { 1077 case TR_STAT_LOCAL_ERROR: string = NSLocalizedString(@"Error", "Torrent -> status string"); break; 1078 case TR_STAT_TRACKER_ERROR: string = NSLocalizedString(@"Tracker returned error", "Torrent -> status string"); break; 1079 case TR_STAT_TRACKER_WARNING: string = NSLocalizedString(@"Tracker returned warning", "Torrent -> status string"); break; 1080 default: NSAssert(NO, @"unknown error state"); 1081 } 1082 1083 NSString * errorString = [self errorMessage]; 1084 if (errorString && ![errorString isEqualToString: @""]) 1085 string = [string stringByAppendingFormat: @": %@", errorString]; 1086 } 1087 else 1088 { 1089 switch (fStat->activity) 1090 { 1091 case TR_STATUS_STOPPED: 1092 if ([self isFinishedSeeding]) 1093 string = NSLocalizedString(@"Seeding complete", "Torrent -> status string"); 1094 else 1095 string = NSLocalizedString(@"Paused", "Torrent -> status string"); 1096 break; 1097 1098 case TR_STATUS_DOWNLOAD_WAIT: 1099 string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis]; 1100 break; 1101 1102 case TR_STATUS_SEED_WAIT: 1103 string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis]; 1104 break; 1105 1106 case TR_STATUS_CHECK_WAIT: 1107 string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis]; 1108 break; 1109 1110 case TR_STATUS_CHECK: 1111 string = [NSString stringWithFormat: @"%@ (%@)", 1112 NSLocalizedString(@"Checking existing data", "Torrent -> status string"), 1113 [NSString percentString: [self checkingProgress] longDecimals: YES]]; 1114 break; 1115 1116 case TR_STATUS_DOWNLOAD: 1117 if ([self totalPeersConnected] != 1) 1118 string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of %d peers", 1119 "Torrent -> status string"), [self peersSendingToUs], [self totalPeersConnected]]; 1120 else 1121 string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of 1 peer", 1122 "Torrent -> status string"), [self peersSendingToUs]]; 1123 1124 const NSInteger webSeedCount = fStat->webseedsSendingToUs; 1125 if (webSeedCount > 0) 1126 { 1127 NSString * webSeedString; 1128 if (webSeedCount == 1) 1129 webSeedString = NSLocalizedString(@"web seed", "Torrent -> status string"); 1130 else 1131 webSeedString = [NSString stringWithFormat: NSLocalizedString(@"%d web seeds", "Torrent -> status string"), 1132 webSeedCount]; 1133 1134 string = [string stringByAppendingFormat: @" + %@", webSeedString]; 1135 } 1136 1137 break; 1138 1139 case TR_STATUS_SEED: 1140 if ([self totalPeersConnected] != 1) 1141 string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of %d peers", "Torrent -> status string"), 1142 [self peersGettingFromUs], [self totalPeersConnected]]; 1143 else 1144 string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of 1 peer", "Torrent -> status string"), 1145 [self peersGettingFromUs]]; 1146 } 1147 1148 if ([self isStalled]) 1149 string = [NSLocalizedString(@"Stalled", "Torrent -> status string") stringByAppendingFormat: @", %@", string]; 1150 } 1151 1152 //append even if error 1153 if ([self isActive] && ![self isChecking]) 1154 { 1155 if (fStat->activity == TR_STATUS_DOWNLOAD) 1156 string = [string stringByAppendingFormat: @" - %@: %@, %@: %@", 1157 NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]], 1158 NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]]; 1159 else 1160 string = [string stringByAppendingFormat: @" - %@: %@", 1161 NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]]; 1162 } 1163 1164 return string; 1165} 1166 1167- (NSString *) shortStatusString 1168{ 1169 NSString * string; 1170 1171 switch (fStat->activity) 1172 { 1173 case TR_STATUS_STOPPED: 1174 if ([self isFinishedSeeding]) 1175 string = NSLocalizedString(@"Seeding complete", "Torrent -> status string"); 1176 else 1177 string = NSLocalizedString(@"Paused", "Torrent -> status string"); 1178 break; 1179 1180 case TR_STATUS_DOWNLOAD_WAIT: 1181 string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis]; 1182 break; 1183 1184 case TR_STATUS_SEED_WAIT: 1185 string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis]; 1186 break; 1187 1188 case TR_STATUS_CHECK_WAIT: 1189 string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis]; 1190 break; 1191 1192 case TR_STATUS_CHECK: 1193 string = [NSString stringWithFormat: @"%@ (%@)", 1194 NSLocalizedString(@"Checking existing data", "Torrent -> status string"), 1195 [NSString percentString: [self checkingProgress] longDecimals: YES]]; 1196 break; 1197 1198 case TR_STATUS_DOWNLOAD: 1199 string = [NSString stringWithFormat: @"%@: %@, %@: %@", 1200 NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]], 1201 NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]]; 1202 break; 1203 1204 case TR_STATUS_SEED: 1205 string = [NSString stringWithFormat: @"%@: %@, %@: %@", 1206 NSLocalizedString(@"Ratio", "Torrent -> status string"), [NSString stringForRatio: [self ratio]], 1207 NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]]; 1208 } 1209 1210 return string; 1211} 1212 1213- (NSString *) remainingTimeString 1214{ 1215 if ([self shouldShowEta]) 1216 return [self etaString]; 1217 else 1218 return [self shortStatusString]; 1219} 1220 1221- (NSString *) stateString 1222{ 1223 switch (fStat->activity) 1224 { 1225 case TR_STATUS_STOPPED: 1226 case TR_STATUS_DOWNLOAD_WAIT: 1227 case TR_STATUS_SEED_WAIT: 1228 { 1229 NSString * string = NSLocalizedString(@"Paused", "Torrent -> status string"); 1230 1231 NSString * extra = nil; 1232 if ([self waitingToStart]) 1233 { 1234 extra = fStat->activity == TR_STATUS_DOWNLOAD_WAIT 1235 ? NSLocalizedString(@"Waiting to download", "Torrent -> status string") 1236 : NSLocalizedString(@"Waiting to seed", "Torrent -> status string"); 1237 } 1238 else if ([self isFinishedSeeding]) 1239 extra = NSLocalizedString(@"Seeding complete", "Torrent -> status string"); 1240 else; 1241 1242 return extra ? [string stringByAppendingFormat: @" (%@)", extra] : string; 1243 } 1244 1245 case TR_STATUS_CHECK_WAIT: 1246 return [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis]; 1247 1248 case TR_STATUS_CHECK: 1249 return [NSString stringWithFormat: @"%@ (%@)", 1250 NSLocalizedString(@"Checking existing data", "Torrent -> status string"), 1251 [NSString percentString: [self checkingProgress] longDecimals: YES]]; 1252 1253 case TR_STATUS_DOWNLOAD: 1254 return NSLocalizedString(@"Downloading", "Torrent -> status string"); 1255 1256 case TR_STATUS_SEED: 1257 return NSLocalizedString(@"Seeding", "Torrent -> status string"); 1258 } 1259} 1260 1261- (NSInteger) totalPeersConnected 1262{ 1263 return fStat->peersConnected; 1264} 1265 1266- (NSInteger) totalPeersTracker 1267{ 1268 return fStat->peersFrom[TR_PEER_FROM_TRACKER]; 1269} 1270 1271- (NSInteger) totalPeersIncoming 1272{ 1273 return fStat->peersFrom[TR_PEER_FROM_INCOMING]; 1274} 1275 1276- (NSInteger) totalPeersCache 1277{ 1278 return fStat->peersFrom[TR_PEER_FROM_RESUME]; 1279} 1280 1281- (NSInteger) totalPeersPex 1282{ 1283 return fStat->peersFrom[TR_PEER_FROM_PEX]; 1284} 1285 1286- (NSInteger) totalPeersDHT 1287{ 1288 return fStat->peersFrom[TR_PEER_FROM_DHT]; 1289} 1290 1291- (NSInteger) totalPeersLocal 1292{ 1293 return fStat->peersFrom[TR_PEER_FROM_LPD]; 1294} 1295 1296- (NSInteger) totalPeersLTEP 1297{ 1298 return fStat->peersFrom[TR_PEER_FROM_LTEP]; 1299} 1300 1301- (NSInteger) peersSendingToUs 1302{ 1303 return fStat->peersSendingToUs; 1304} 1305 1306- (NSInteger) peersGettingFromUs 1307{ 1308 return fStat->peersGettingFromUs; 1309} 1310 1311- (CGFloat) downloadRate 1312{ 1313 return fStat->pieceDownloadSpeed_KBps; 1314} 1315 1316- (CGFloat) uploadRate 1317{ 1318 return fStat->pieceUploadSpeed_KBps; 1319} 1320 1321- (CGFloat) totalRate 1322{ 1323 return [self downloadRate] + [self uploadRate]; 1324} 1325 1326- (uint64_t) haveVerified 1327{ 1328 return fStat->haveValid; 1329} 1330 1331- (uint64_t) haveTotal 1332{ 1333 return [self haveVerified] + fStat->haveUnchecked; 1334} 1335 1336- (uint64_t) totalSizeSelected 1337{ 1338 return fStat->sizeWhenDone; 1339} 1340 1341- (uint64_t) downloadedTotal 1342{ 1343 return fStat->downloadedEver; 1344} 1345 1346- (uint64_t) uploadedTotal 1347{ 1348 return fStat->uploadedEver; 1349} 1350 1351- (uint64_t) failedHash 1352{ 1353 return fStat->corruptEver; 1354} 1355 1356- (NSInteger) groupValue 1357{ 1358 return fGroupValue; 1359} 1360 1361- (void) setGroupValue: (NSInteger) groupValue determinationType: (TorrentDeterminationType) determinationType 1362{ 1363 if (groupValue != fGroupValue) 1364 { 1365 fGroupValue = groupValue; 1366 [[NSNotificationCenter defaultCenter] postNotificationName: kTorrentDidChangeGroupNotification object: self]; 1367 } 1368 fGroupValueDetermination = determinationType; 1369} 1370 1371- (NSInteger) groupOrderValue 1372{ 1373 return [[GroupsController groups] rowValueForIndex: fGroupValue]; 1374} 1375 1376- (void) checkGroupValueForRemoval: (NSNotification *) notification 1377{ 1378 if (fGroupValue != -1 && [[notification userInfo][@"Index"] integerValue] == fGroupValue) 1379 fGroupValue = -1; 1380} 1381 1382- (NSArray *) fileList 1383{ 1384 return fFileList; 1385} 1386 1387- (NSArray *) flatFileList 1388{ 1389 return fFlatFileList; 1390} 1391 1392- (NSInteger) fileCount 1393{ 1394 return fInfo->fileCount; 1395} 1396 1397- (void) updateFileStat 1398{ 1399 if (fFileStat) 1400 tr_torrentFilesFree(fFileStat, [self fileCount]); 1401 1402 fFileStat = tr_torrentFiles(fHandle, NULL); 1403} 1404 1405- (CGFloat) fileProgress: (FileListNode *) node 1406{ 1407 if ([self fileCount] == 1 || [self isComplete]) 1408 return [self progress]; 1409 1410 if (!fFileStat) 1411 [self updateFileStat]; 1412 1413 // #5501 1414 if ([node size] == 0) { 1415 return 1.0; 1416 } 1417 1418 NSIndexSet * indexSet = [node indexes]; 1419 1420 if ([indexSet count] == 1) 1421 return fFileStat[[indexSet firstIndex]].progress; 1422 1423 uint64_t have = 0; 1424 for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) 1425 have += fFileStat[index].bytesCompleted; 1426 1427 return (CGFloat)have / [node size]; 1428} 1429 1430- (BOOL) canChangeDownloadCheckForFile: (NSUInteger) index 1431{ 1432 NSAssert2((NSInteger)index < [self fileCount], @"Index %ld is greater than file count %ld", index, [self fileCount]); 1433 1434 return [self canChangeDownloadCheckForFiles: [NSIndexSet indexSetWithIndex: index]]; 1435} 1436 1437- (BOOL) canChangeDownloadCheckForFiles: (NSIndexSet *) indexSet 1438{ 1439 if ([self fileCount] == 1 || [self isComplete]) 1440 return NO; 1441 1442 if (!fFileStat) 1443 [self updateFileStat]; 1444 1445 __block BOOL canChange = NO; 1446 [indexSet enumerateIndexesWithOptions: NSEnumerationConcurrent usingBlock: ^(NSUInteger index, BOOL *stop) { 1447 if (fFileStat[index].progress < 1.0) 1448 { 1449 canChange = YES; 1450 *stop = YES; 1451 } 1452 }]; 1453 return canChange; 1454} 1455 1456- (NSInteger) checkForFiles: (NSIndexSet *) indexSet 1457{ 1458 BOOL onState = NO, offState = NO; 1459 for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) 1460 { 1461 if (!fInfo->files[index].dnd || ![self canChangeDownloadCheckForFile: index]) 1462 onState = YES; 1463 else 1464 offState = YES; 1465 1466 if (onState && offState) 1467 return NSMixedState; 1468 } 1469 return onState ? NSOnState : NSOffState; 1470} 1471 1472- (void) setFileCheckState: (NSInteger) state forIndexes: (NSIndexSet *) indexSet 1473{ 1474 NSUInteger count = [indexSet count]; 1475 tr_file_index_t * files = malloc(count * sizeof(tr_file_index_t)); 1476 for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++) 1477 files[i] = index; 1478 1479 tr_torrentSetFileDLs(fHandle, files, count, state != NSOffState); 1480 free(files); 1481 1482 [self update]; 1483 [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFileCheckChange" object: self]; 1484} 1485 1486- (void) setFilePriority: (tr_priority_t) priority forIndexes: (NSIndexSet *) indexSet 1487{ 1488 const NSUInteger count = [indexSet count]; 1489 tr_file_index_t * files = tr_malloc(count * sizeof(tr_file_index_t)); 1490 for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++) 1491 files[i] = index; 1492 1493 tr_torrentSetFilePriorities(fHandle, files, count, priority); 1494 tr_free(files); 1495} 1496 1497- (BOOL) hasFilePriority: (tr_priority_t) priority forIndexes: (NSIndexSet *) indexSet 1498{ 1499 for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) 1500 if (priority == fInfo->files[index].priority && [self canChangeDownloadCheckForFile: index]) 1501 return YES; 1502 return NO; 1503} 1504 1505- (NSSet *) filePrioritiesForIndexes: (NSIndexSet *) indexSet 1506{ 1507 BOOL low = NO, normal = NO, high = NO; 1508 NSMutableSet * priorities = [NSMutableSet setWithCapacity: MIN([indexSet count], 3u)]; 1509 1510 for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) 1511 { 1512 if (![self canChangeDownloadCheckForFile: index]) 1513 continue; 1514 1515 const tr_priority_t priority = fInfo->files[index].priority; 1516 switch (priority) 1517 { 1518 case TR_PRI_LOW: 1519 if (low) 1520 continue; 1521 low = YES; 1522 break; 1523 case TR_PRI_NORMAL: 1524 if (normal) 1525 continue; 1526 normal = YES; 1527 break; 1528 case TR_PRI_HIGH: 1529 if (high) 1530 continue; 1531 high = YES; 1532 break; 1533 default: 1534 NSAssert2(NO, @"Unknown priority %d for file index %ld", priority, index); 1535 } 1536 1537 [priorities addObject: @(priority)]; 1538 if (low && normal && high) 1539 break; 1540 } 1541 return priorities; 1542} 1543 1544- (NSDate *) dateAdded 1545{ 1546 const time_t date = fStat->addedDate; 1547 return [NSDate dateWithTimeIntervalSince1970: date]; 1548} 1549 1550- (NSDate *) dateCompleted 1551{ 1552 const time_t date = fStat->doneDate; 1553 return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil; 1554} 1555 1556- (NSDate *) dateActivity 1557{ 1558 const time_t date = fStat->activityDate; 1559 return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil; 1560} 1561 1562- (NSDate *) dateActivityOrAdd 1563{ 1564 NSDate * date = [self dateActivity]; 1565 return date ? date : [self dateAdded]; 1566} 1567 1568- (NSInteger) secondsDownloading 1569{ 1570 return fStat->secondsDownloading; 1571} 1572 1573- (NSInteger) secondsSeeding 1574{ 1575 return fStat->secondsSeeding; 1576} 1577 1578- (NSInteger) stalledMinutes 1579{ 1580 if (fStat->idleSecs == -1) 1581 return -1; 1582 1583 return fStat->idleSecs / 60; 1584} 1585 1586- (BOOL) isStalled 1587{ 1588 return fStat->isStalled; 1589} 1590 1591- (void) updateTimeMachineExclude 1592{ 1593 [self setTimeMachineExclude: ![self allDownloaded]]; 1594} 1595 1596- (NSInteger) stateSortKey 1597{ 1598 if (![self isActive]) //paused 1599 { 1600 if ([self waitingToStart]) 1601 return 1; 1602 else 1603 return 0; 1604 } 1605 else if ([self isSeeding]) //seeding 1606 return 10; 1607 else //downloading 1608 return 20; 1609} 1610 1611- (NSString *) trackerSortKey 1612{ 1613 int count; 1614 tr_tracker_stat * stats = tr_torrentTrackers(fHandle, &count); 1615 1616 NSString * best = nil; 1617 1618 for (int i=0; i < count; ++i) 1619 { 1620 NSString * tracker = @(stats[i].host); 1621 if (!best || [tracker localizedCaseInsensitiveCompare: best] == NSOrderedAscending) 1622 best = tracker; 1623 } 1624 1625 tr_torrentTrackersFree(stats, count); 1626 return best; 1627} 1628 1629- (tr_torrent *) torrentStruct 1630{ 1631 return fHandle; 1632} 1633 1634- (NSURL *) previewItemURL 1635{ 1636 NSString * location = [self dataLocation]; 1637 return location ? [NSURL fileURLWithPath: location] : nil; 1638} 1639 1640@end 1641 1642@implementation Torrent (Private) 1643 1644- (id) initWithPath: (NSString *) path hash: (NSString *) hashString torrentStruct: (tr_torrent *) torrentStruct 1645 magnetAddress: (NSString *) magnetAddress lib: (tr_session *) lib 1646 groupValue: (NSNumber *) groupValue 1647 removeWhenFinishSeeding: (NSNumber *) removeWhenFinishSeeding 1648 downloadFolder: (NSString *) downloadFolder 1649 legacyIncompleteFolder: (NSString *) incompleteFolder 1650{ 1651 if (!(self = [super init])) 1652 return nil; 1653 1654 fDefaults = [NSUserDefaults standardUserDefaults]; 1655 1656 if (torrentStruct) 1657 fHandle = torrentStruct; 1658 else 1659 { 1660 //set libtransmission settings for initialization 1661 tr_ctor * ctor = tr_ctorNew(lib); 1662 1663 tr_ctorSetPaused(ctor, TR_FORCE, YES); 1664 if (downloadFolder) 1665 tr_ctorSetDownloadDir(ctor, TR_FORCE, [downloadFolder UTF8String]); 1666 if (incompleteFolder) 1667 tr_ctorSetIncompleteDir(ctor, [incompleteFolder UTF8String]); 1668 1669 tr_parse_result result = TR_PARSE_ERR; 1670 if (path) 1671 result = tr_ctorSetMetainfoFromFile(ctor, [path UTF8String]); 1672 1673 if (result != TR_PARSE_OK && magnetAddress) 1674 result = tr_ctorSetMetainfoFromMagnetLink(ctor, [magnetAddress UTF8String]); 1675 1676 //backup - shouldn't be needed after upgrade to 1.70 1677 if (result != TR_PARSE_OK && hashString) 1678 result = tr_ctorSetMetainfoFromHash(ctor, [hashString UTF8String]); 1679 1680 if (result == TR_PARSE_OK) 1681 fHandle = tr_torrentNew(ctor, NULL, NULL); 1682 1683 tr_ctorFree(ctor); 1684 1685 if (!fHandle) 1686 { 1687 return nil; 1688 } 1689 } 1690 1691 fInfo = tr_torrentInfo(fHandle); 1692 1693 tr_torrentSetQueueStartCallback(fHandle, startQueueCallback, (__bridge void *)(self)); 1694 tr_torrentSetCompletenessCallback(fHandle, completenessChangeCallback, (__bridge void *)(self)); 1695 tr_torrentSetRatioLimitHitCallback(fHandle, ratioLimitHitCallback, (__bridge void *)(self)); 1696 tr_torrentSetIdleLimitHitCallback(fHandle, idleLimitHitCallback, (__bridge void *)(self)); 1697 tr_torrentSetMetadataCallback(fHandle, metadataCallback, (__bridge void *)(self)); 1698 1699 fHashString = [[NSString alloc] initWithUTF8String: fInfo->hashString]; 1700 1701 fResumeOnWake = NO; 1702 1703 //don't do after this point - it messes with auto-group functionality 1704 if (![self isMagnet]) 1705 [self createFileList]; 1706 1707 fDownloadFolderDetermination = TorrentDeterminationAutomatic; 1708 1709 if (groupValue) 1710 { 1711 fGroupValueDetermination = TorrentDeterminationUserSpecified; 1712 fGroupValue = [groupValue intValue]; 1713 } 1714 else 1715 { 1716 fGroupValueDetermination = TorrentDeterminationAutomatic; 1717 fGroupValue = [[GroupsController groups] groupIndexForTorrent: self]; 1718 } 1719 1720 fRemoveWhenFinishSeeding = removeWhenFinishSeeding ? [removeWhenFinishSeeding boolValue] : [fDefaults boolForKey: @"RemoveWhenFinishSeeding"]; 1721 1722 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(checkGroupValueForRemoval:) 1723 name: @"GroupValueRemoved" object: nil]; 1724 1725 fTimeMachineExcludeInitialized = NO; 1726 [self update]; 1727 1728 return self; 1729} 1730 1731- (void) createFileList 1732{ 1733 NSAssert(![self isMagnet], @"Cannot create a file list until the torrent is demagnetized"); 1734 1735 if ([self isFolder]) 1736 { 1737 const NSInteger count = [self fileCount]; 1738 NSMutableArray * flatFileList = [NSMutableArray arrayWithCapacity: count]; 1739 1740 FileListNode * tempNode = nil; 1741 1742 for (NSInteger i = 0; i < count; i++) 1743 { 1744 tr_file * file = &fInfo->files[i]; 1745 1746 NSString * fullPath = @(file->name); 1747 NSArray * pathComponents = [fullPath pathComponents]; 1748 1749 if (!tempNode) 1750 tempNode = [[FileListNode alloc] initWithFolderName:pathComponents[0] path:@"" torrent:self]; 1751 1752 [self insertPathForComponents: pathComponents withComponentIndex: 1 forParent: tempNode fileSize: file->length index: i flatList: flatFileList]; 1753 } 1754 1755 [self sortFileList: [tempNode children]]; 1756 [self sortFileList: flatFileList]; 1757 1758 fFileList = [[NSArray alloc] initWithArray: [tempNode children]]; 1759 fFlatFileList = [[NSArray alloc] initWithArray: flatFileList]; 1760 } 1761 else 1762 { 1763 FileListNode * node = [[FileListNode alloc] initWithFileName: [self name] path: @"" size: [self size] index: 0 torrent: self]; 1764 fFileList = @[node]; 1765 fFlatFileList = fFileList; 1766 } 1767} 1768 1769- (void) insertPathForComponents: (NSArray *) components withComponentIndex: (NSUInteger) componentIndex forParent: (FileListNode *) parent fileSize: (uint64_t) size 1770 index: (NSInteger) index flatList: (NSMutableArray *) flatFileList 1771{ 1772 NSParameterAssert([components count] > 0); 1773 NSParameterAssert(componentIndex < [components count]); 1774 1775 NSString * name = components[componentIndex]; 1776 const BOOL isFolder = componentIndex < ([components count]-1); 1777 1778 //determine if folder node already exists 1779 __block FileListNode * node = nil; 1780 if (isFolder) 1781 { 1782 [[parent children] enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * searchNode, NSUInteger idx, BOOL * stop) { 1783 if ([[searchNode name] isEqualToString: name] && [searchNode isFolder]) 1784 { 1785 node = searchNode; 1786 *stop = YES; 1787 } 1788 }]; 1789 } 1790 1791 //create new folder or file if it doesn't already exist 1792 if (!node) 1793 { 1794 NSString * path = [[parent path] stringByAppendingPathComponent: [parent name]]; 1795 if (isFolder) 1796 node = [[FileListNode alloc] initWithFolderName: name path: path torrent: self]; 1797 else 1798 { 1799 node = [[FileListNode alloc] initWithFileName: name path: path size: size index: index torrent: self]; 1800 [flatFileList addObject: node]; 1801 } 1802 1803 [parent insertChild: node]; 1804 } 1805 1806 if (isFolder) 1807 { 1808 [node insertIndex: index withSize: size]; 1809 1810 [self insertPathForComponents: components withComponentIndex: (componentIndex+1) forParent: node fileSize: size index: index flatList: flatFileList]; 1811 } 1812} 1813 1814- (void) sortFileList: (NSMutableArray *) fileNodes 1815{ 1816 NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey: @"name" ascending: YES selector: @selector(localizedStandardCompare:)]; 1817 [fileNodes sortUsingDescriptors: @[descriptor]]; 1818 1819 [fileNodes enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * node, NSUInteger idx, BOOL * stop) { 1820 if ([node isFolder]) 1821 [self sortFileList: [node children]]; 1822 }]; 1823} 1824 1825- (void) startQueue 1826{ 1827 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self]; 1828} 1829 1830- (void) completenessChange: (tr_completeness) status wasRunning: (BOOL) wasRunning 1831{ 1832 fStat = tr_torrentStat(fHandle); //don't call update yet to avoid auto-stop 1833 1834 switch (status) 1835 { 1836 case TR_SEED: 1837 case TR_PARTIAL_SEED: 1838 { 1839 NSDictionary * statusInfo = @{ @"Status" : @(status), @"WasRunning" : @(wasRunning) }; 1840 [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedDownloading" object: self userInfo: statusInfo]; 1841 1842 //quarantine the finished data 1843 NSString * dataLocation = [[self currentDirectory] stringByAppendingPathComponent: [self name]]; 1844 NSDictionary * quarantineProperties = @{ (NSString *)kLSQuarantineTypeKey : (NSString *)kLSQuarantineTypeOtherDownload }; 1845 if ([NSApp isOnYosemiteOrBetter]) 1846 { 1847 NSURL * dataLocationUrl = [NSURL fileURLWithPath: dataLocation]; 1848 NSError * error = nil; 1849 if (![dataLocationUrl setResourceValue: quarantineProperties forKey: NSURLQuarantinePropertiesKey error: &error]) 1850 NSLog(@"Failed to quarantine %@: %@", dataLocation, [error description]); 1851 } 1852 else 1853 { 1854 NSString * dataLocation = [[self currentDirectory] stringByAppendingPathComponent: [self name]]; 1855 FSRef ref; 1856 if (FSPathMakeRef((const UInt8 *)[dataLocation UTF8String], &ref, NULL) == noErr) 1857 { 1858 if (LSSetItemAttribute(&ref, kLSRolesAll, kLSItemQuarantineProperties, (__bridge CFTypeRef)(quarantineProperties)) != noErr) 1859 NSLog(@"Failed to quarantine: %@", dataLocation); 1860 } 1861 else 1862 NSLog(@"Could not find file to quarantine: %@", dataLocation); 1863 } 1864 break; 1865 } 1866 case TR_LEECH: 1867 [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentRestartedDownloading" object: self]; 1868 break; 1869 } 1870 1871 [self update]; 1872 [self updateTimeMachineExclude]; 1873} 1874 1875- (void) ratioLimitHit 1876{ 1877 fStat = tr_torrentStat(fHandle); 1878 1879 [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedSeeding" object: self]; 1880} 1881 1882- (void) idleLimitHit 1883{ 1884 fStat = tr_torrentStat(fHandle); 1885 1886 [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedSeeding" object: self]; 1887} 1888 1889- (void) metadataRetrieved 1890{ 1891 fStat = tr_torrentStat(fHandle); 1892 1893 [self createFileList]; 1894 1895 /* If the torrent is in no group, or the group was automatically determined based on criteria evaluated 1896 * before we had metadata for this torrent, redetermine the group 1897 */ 1898 if ((fGroupValueDetermination == TorrentDeterminationAutomatic) || ([self groupValue] == -1)) 1899 [self setGroupValue: [[GroupsController groups] groupIndexForTorrent: self] determinationType: TorrentDeterminationAutomatic]; 1900 1901 //change the location if the group calls for it and it's either not already set or was set automatically before 1902 if (((fDownloadFolderDetermination == TorrentDeterminationAutomatic) || !tr_torrentGetCurrentDir(fHandle)) && 1903 [[GroupsController groups] usesCustomDownloadLocationForIndex: [self groupValue]]) 1904 { 1905 NSString *location = [[GroupsController groups] customDownloadLocationForIndex: [self groupValue]]; 1906 [self changeDownloadFolderBeforeUsing: location determinationType:TorrentDeterminationAutomatic]; 1907 } 1908 1909 [[NSNotificationCenter defaultCenter] postNotificationName: @"ResetInspector" object: self userInfo: @{ @"Torrent" : self }]; 1910} 1911 1912- (void)renameFinished: (BOOL) success nodes: (NSArray *) nodes completionHandler: (void (^)(BOOL)) completionHandler oldPath: (NSString *) oldPath newName: (NSString *) newName 1913{ 1914 NSParameterAssert(completionHandler != nil); 1915 NSParameterAssert(oldPath != nil); 1916 NSParameterAssert(newName != nil); 1917 1918 NSString * path = [oldPath stringByDeletingLastPathComponent]; 1919 1920 if (success) 1921 { 1922 NSString * oldName = [oldPath lastPathComponent]; 1923 void (^__block __weak weakUpdateNodeAndChildrenForRename)(FileListNode *); 1924 void (^updateNodeAndChildrenForRename)(FileListNode *); 1925 weakUpdateNodeAndChildrenForRename = updateNodeAndChildrenForRename = ^(FileListNode * node) { 1926 [node updateFromOldName: oldName toNewName: newName inPath: path]; 1927 1928 if ([node isFolder]) { 1929 [[node children] enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * childNode, NSUInteger idx, BOOL * stop) { 1930 weakUpdateNodeAndChildrenForRename(childNode); 1931 }]; 1932 } 1933 }; 1934 1935 if (!nodes) 1936 nodes = fFlatFileList; 1937 [nodes enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * node, NSUInteger idx, BOOL *stop) { 1938 updateNodeAndChildrenForRename(node); 1939 }]; 1940 1941 //resort lists 1942 NSMutableArray * fileList = [fFileList mutableCopy]; 1943 [self sortFileList: fileList]; 1944 fFileList = fileList; 1945 1946 NSMutableArray * flatFileList = [fFlatFileList mutableCopy]; 1947 [self sortFileList: flatFileList]; 1948 fFlatFileList = flatFileList; 1949 1950 fIcon = nil; 1951 } 1952 else 1953 NSLog(@"Error renaming %@ to %@", oldPath, [path stringByAppendingPathComponent: newName]); 1954 1955 completionHandler(success); 1956} 1957 1958- (BOOL) shouldShowEta 1959{ 1960 if (fStat->activity == TR_STATUS_DOWNLOAD) 1961 return YES; 1962 else if ([self isSeeding]) 1963 { 1964 //ratio: show if it's set at all 1965 if (tr_torrentGetSeedRatio(fHandle, NULL)) 1966 return YES; 1967 1968 //idle: show only if remaining time is less than cap 1969 if (fStat->etaIdle != TR_ETA_NOT_AVAIL && fStat->etaIdle < ETA_IDLE_DISPLAY_SEC) 1970 return YES; 1971 } 1972 1973 return NO; 1974} 1975 1976- (NSString *) etaString 1977{ 1978 NSInteger eta; 1979 BOOL fromIdle; 1980 //don't check for both, since if there's a regular ETA, the torrent isn't idle so it's meaningless 1981 if (fStat->eta != TR_ETA_NOT_AVAIL && fStat->eta != TR_ETA_UNKNOWN) 1982 { 1983 eta = fStat->eta; 1984 fromIdle = NO; 1985 } 1986 else if (fStat->etaIdle != TR_ETA_NOT_AVAIL && fStat->etaIdle < ETA_IDLE_DISPLAY_SEC) 1987 { 1988 eta = fStat->etaIdle; 1989 fromIdle = YES; 1990 } 1991 else 1992 return NSLocalizedString(@"remaining time unknown", "Torrent -> eta string"); 1993 1994 NSString * idleString; 1995 1996 if ([NSApp isOnYosemiteOrBetter]) { 1997 static NSDateComponentsFormatter *formatter; 1998 static dispatch_once_t onceToken; 1999 dispatch_once(&onceToken, ^{ 2000 formatter = [NSDateComponentsFormatter new]; 2001 formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleShort; 2002 formatter.maximumUnitCount = 2; 2003 formatter.collapsesLargestUnit = YES; 2004 formatter.includesTimeRemainingPhrase = YES; 2005 }); 2006 2007 idleString = [formatter stringFromTimeInterval: eta]; 2008 } 2009 else { 2010 idleString = [NSString timeString: eta includesTimeRemainingPhrase: YES showSeconds: YES maxFields: 2]; 2011 } 2012 2013 if (fromIdle) { 2014 idleString = [idleString stringByAppendingFormat: @" (%@)", NSLocalizedString(@"inactive", "Torrent -> eta string")]; 2015 } 2016 2017 return idleString; 2018} 2019 2020- (void) setTimeMachineExclude: (BOOL) exclude 2021{ 2022 NSString * path; 2023 if ((path = [self dataLocation])) 2024 { 2025 CSBackupSetItemExcluded((__bridge CFURLRef)[NSURL fileURLWithPath: path], exclude, false); 2026 fTimeMachineExcludeInitialized = YES; 2027 } 2028} 2029 2030@end 2031