Transmission: update from 2.42 to 2.50
[tomato.git] / release / src / router / transmission / macosx / Torrent.m
blob2bca44b51bbac5cb00909d5d6e427a8e9e0fd0cc
1 /******************************************************************************
2  * $Id: Torrent.m 13219 2012-02-11 05:13:46Z livings124 $
3  *
4  * Copyright (c) 2006-2012 Transmission authors and contributors
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a
7  * copy of this software and associated documentation files (the "Software"),
8  * to deal in the Software without restriction, including without limitation
9  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10  * and/or sell copies of the Software, and to permit persons to whom the
11  * Software is furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22  * DEALINGS IN THE SOFTWARE.
23  *****************************************************************************/
25 #import "Torrent.h"
26 #import "GroupsController.h"
27 #import "FileListNode.h"
28 #import "NSStringAdditions.h"
29 #import "TrackerNode.h"
31 #import "transmission.h" // required by utils.h
32 #import "utils.h" // tr_new()
34 #define ETA_IDLE_DISPLAY_SEC (2*60)
36 @interface Torrent (Private)
38 - (id) initWithPath: (NSString *) path hash: (NSString *) hashString torrentStruct: (tr_torrent *) torrentStruct
39         magnetAddress: (NSString *) magnetAddress lib: (tr_session *) lib
40         groupValue: (NSNumber *) groupValue
41         timeMachineExcludeLocation: (NSString *) timeMachineExclude
42         downloadFolder: (NSString *) downloadFolder
43         legacyIncompleteFolder: (NSString *) incompleteFolder;
45 - (void) createFileList;
46 - (void) insertPath: (NSMutableArray *) components forParent: (FileListNode *) parent fileSize: (uint64_t) size
47     index: (NSInteger) index flatList: (NSMutableArray *) flatFileList;
48 - (void) sortFileList: (NSMutableArray *) fileNodes;
50 - (void) startQueue;
51 - (void) completenessChange: (NSDictionary *) statusInfo;
52 - (void) ratioLimitHit;
53 - (void) idleLimitHit;
54 - (void) metadataRetrieved;
56 - (BOOL) shouldShowEta;
57 - (NSString *) etaString;
59 - (void) setTimeMachineExclude: (BOOL) exclude forPath: (NSString *) path;
61 @end
63 void startQueueCallback(tr_torrent * torrent, void * torrentData)
65     [(Torrent *)torrentData performSelectorOnMainThread: @selector(startQueue) withObject: nil waitUntilDone: NO];
68 void completenessChangeCallback(tr_torrent * torrent, tr_completeness status, bool wasRunning, void * torrentData)
70     @autoreleasepool
71     {
72         NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: status], @"Status",
73                                [NSNumber numberWithBool: wasRunning], @"WasRunning", nil];
74         [(Torrent *)torrentData performSelectorOnMainThread: @selector(completenessChange:) withObject: dict waitUntilDone: NO];
75     }
78 void ratioLimitHitCallback(tr_torrent * torrent, void * torrentData)
80     [(Torrent *)torrentData performSelectorOnMainThread: @selector(ratioLimitHit) withObject: nil waitUntilDone: NO];
83 void idleLimitHitCallback(tr_torrent * torrent, void * torrentData)
85     [(Torrent *)torrentData performSelectorOnMainThread: @selector(idleLimitHit) withObject: nil waitUntilDone: NO];
88 void metadataCallback(tr_torrent * torrent, void * torrentData)
90     [(Torrent *)torrentData performSelectorOnMainThread: @selector(metadataRetrieved) withObject: nil waitUntilDone: NO];
93 int trashDataFile(const char * filename)
95     @autoreleasepool
96     {
97         if (filename != NULL)
98             [Torrent trashFile: [NSString stringWithUTF8String: filename]];
99     }
100     return 0;
103 @implementation Torrent
105 - (id) initWithPath: (NSString *) path location: (NSString *) location deleteTorrentFile: (BOOL) torrentDelete
106         lib: (tr_session *) lib
108     self = [self initWithPath: path hash: nil torrentStruct: NULL magnetAddress: nil lib: lib
109             groupValue: nil
110             timeMachineExcludeLocation: nil
111             downloadFolder: location
112             legacyIncompleteFolder: nil];
113     
114     if (self)
115     {
116         if (torrentDelete && ![[self torrentLocation] isEqualToString: path])
117             [Torrent trashFile: path];
118     }
119     return self;
122 - (id) initWithTorrentStruct: (tr_torrent *) torrentStruct location: (NSString *) location lib: (tr_session *) lib
124     self = [self initWithPath: nil hash: nil torrentStruct: torrentStruct magnetAddress: nil lib: lib
125             groupValue: nil
126             timeMachineExcludeLocation: nil
127             downloadFolder: location
128             legacyIncompleteFolder: nil];
129     
130     return self;
133 - (id) initWithMagnetAddress: (NSString *) address location: (NSString *) location lib: (tr_session *) lib
135     self = [self initWithPath: nil hash: nil torrentStruct: nil magnetAddress: address
136             lib: lib groupValue: nil
137             timeMachineExcludeLocation: nil
138             downloadFolder: location legacyIncompleteFolder: nil];
139     
140     return self;
143 - (id) initWithHistory: (NSDictionary *) history lib: (tr_session *) lib forcePause: (BOOL) pause
145     self = [self initWithPath: [history objectForKey: @"InternalTorrentPath"]
146                 hash: [history objectForKey: @"TorrentHash"]
147                 torrentStruct: NULL
148                 magnetAddress: nil
149                 lib: lib
150                 groupValue: [history objectForKey: @"GroupValue"]
151                 timeMachineExcludeLocation: [history objectForKey: @"TimeMachineExcludeLocation"]
152                 downloadFolder: [history objectForKey: @"DownloadFolder"] //upgrading from versions < 1.80
153                 legacyIncompleteFolder: [[history objectForKey: @"UseIncompleteFolder"] boolValue] //upgrading from versions < 1.80
154                                         ? [history objectForKey: @"IncompleteFolder"] : nil];
155     
156     if (self)
157     {
158         //start transfer
159         NSNumber * active;
160         if (!pause && (active = [history objectForKey: @"Active"]) && [active boolValue])
161         {
162             fStat = tr_torrentStat(fHandle);
163             [self startTransferNoQueue];
164         }
165         
166         //upgrading from versions < 1.30: get old added, activity, and done dates
167         NSDate * date;
168         if ((date = [history objectForKey: @"Date"]))
169             tr_torrentSetAddedDate(fHandle, [date timeIntervalSince1970]);
170         if ((date = [history objectForKey: @"DateActivity"]))
171             tr_torrentSetActivityDate(fHandle, [date timeIntervalSince1970]);
172         if ((date = [history objectForKey: @"DateCompleted"]))
173             tr_torrentSetDoneDate(fHandle, [date timeIntervalSince1970]);
174         
175         //upgrading from versions < 1.60: get old stop ratio settings
176         NSNumber * ratioSetting;
177         if ((ratioSetting = [history objectForKey: @"RatioSetting"]))
178         {
179             switch ([ratioSetting intValue])
180             {
181                 case NSOnState: [self setRatioSetting: TR_RATIOLIMIT_SINGLE]; break;
182                 case NSOffState: [self setRatioSetting: TR_RATIOLIMIT_UNLIMITED]; break;
183                 case NSMixedState: [self setRatioSetting: TR_RATIOLIMIT_GLOBAL]; break;
184             }
185         }
186         NSNumber * ratioLimit;
187         if ((ratioLimit = [history objectForKey: @"RatioLimit"]))
188             [self setRatioLimit: [ratioLimit floatValue]];
189     }
190     return self;
193 - (NSDictionary *) history
195     NSMutableDictionary * history = [NSMutableDictionary dictionaryWithObjectsAndKeys:
196                                         [self torrentLocation], @"InternalTorrentPath",
197                                         [self hashString], @"TorrentHash",
198                                         [NSNumber numberWithBool: [self isActive]], @"Active",
199                                         [NSNumber numberWithBool: [self waitingToStart]], @"WaitToStart",
200                                         [NSNumber numberWithInt: fGroupValue], @"GroupValue", nil];
201     
202     if (fTimeMachineExclude)
203         [history setObject: fTimeMachineExclude forKey: @"TimeMachineExcludeLocation"];
204     
205     return history;
208 - (void) dealloc
210     [[NSNotificationCenter defaultCenter] removeObserver: self];
211     
212     if (fFileStat)
213         tr_torrentFilesFree(fFileStat, [self fileCount]);
214     
215     [fPreviousFinishedIndexes release];
216     [fPreviousFinishedIndexesDate release];
217     
218     [fHashString release];
219     
220     [fIcon release];
221     
222     [fFileList release];
223     [fFlatFileList release];
224     
225     [fTimeMachineExclude release];
226     
227     [super dealloc];
230 - (NSString *) description
232     return [@"Torrent: " stringByAppendingString: [self name]];
235 - (id) copyWithZone: (NSZone *) zone
237     return [self retain];
240 - (void) closeRemoveTorrent: (BOOL) trashFiles
242     //allow the file to be indexed by Time Machine
243     if (fTimeMachineExclude)
244     {
245         [self setTimeMachineExclude: NO forPath: fTimeMachineExclude];
246         [fTimeMachineExclude release];
247         fTimeMachineExclude = nil;
248     }
249     
250     tr_torrentRemove(fHandle, trashFiles, trashDataFile);
253 - (void) changeDownloadFolderBeforeUsing: (NSString *) folder
255      tr_torrentSetDownloadDir(fHandle, [folder UTF8String]);
256      [self updateTimeMachineExclude];
259 - (NSString *) currentDirectory
261     return [NSString stringWithUTF8String: tr_torrentGetCurrentDir(fHandle)];
264 - (void) getAvailability: (int8_t *) tab size: (NSInteger) size
266     tr_torrentAvailability(fHandle, tab, size);
269 - (void) getAmountFinished: (float *) tab size: (NSInteger) size
271     tr_torrentAmountFinished(fHandle, tab, size);
274 - (NSIndexSet *) previousFinishedPieces
276     //if the torrent hasn't been seen in a bit, and therefore hasn't been refreshed, return nil
277     if (fPreviousFinishedIndexesDate && [fPreviousFinishedIndexesDate timeIntervalSinceNow] > -2.0)
278         return fPreviousFinishedIndexes;
279     else
280         return nil;
283 - (void) setPreviousFinishedPieces: (NSIndexSet *) indexes
285     [fPreviousFinishedIndexes release];
286     fPreviousFinishedIndexes = [indexes retain];
287     
288     [fPreviousFinishedIndexesDate release];
289     fPreviousFinishedIndexesDate = indexes != nil ? [[NSDate alloc] init] : nil;
292 - (void) update
294     //get previous stalled value before update
295     const BOOL wasStalled = fStat != NULL && [self isStalled];
296     
297     fStat = tr_torrentStat(fHandle);
298     
299     //make sure the "active" filter is updated when stalled-ness changes
300     if (wasStalled != [self isStalled])
301         [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
302     
303     //when the data first appears, update time machine exclusion
304     if (!fTimeMachineExclude)
305         [self updateTimeMachineExclude];
308 - (void) startTransferIgnoringQueue: (BOOL) ignoreQueue
310     if ([self alertForRemainingDiskSpace])
311     {
312         ignoreQueue ? tr_torrentStartNow(fHandle) : tr_torrentStart(fHandle);
313         [self update];
314         
315         //capture, specifically, stop-seeding settings changing to unlimited
316         [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil];
317     }
320 - (void) startTransferNoQueue
322     [self startTransferIgnoringQueue: YES];
325 - (void) startTransfer
327     [self startTransferIgnoringQueue: NO];
330 - (void) stopTransfer
332     tr_torrentStop(fHandle);
333     [self update];
336 - (void) sleep
338     if ((fResumeOnWake = [self isActive]))
339         tr_torrentStop(fHandle);
342 - (void) wakeUp
344     if (fResumeOnWake)
345     {
346         tr_ninf( fInfo->name, "restarting because of wakeUp" );
347         tr_torrentStart(fHandle);
348     }
351 - (NSInteger) queuePosition
353     return fStat->queuePosition;
356 - (void) setQueuePosition: (NSUInteger) index
358     tr_torrentSetQueuePosition(fHandle, index);
361 - (void) manualAnnounce
363     tr_torrentManualUpdate(fHandle);
366 - (BOOL) canManualAnnounce
368     return tr_torrentCanManualUpdate(fHandle);
371 - (void) resetCache
373     tr_torrentVerify(fHandle);
374     [self update];
377 - (BOOL) isMagnet
379     return !tr_torrentHasMetadata(fHandle);
382 - (NSString *) magnetLink
384     return [NSString stringWithUTF8String: tr_torrentGetMagnetLink(fHandle)];
387 - (CGFloat) ratio
389     return fStat->ratio;
392 - (tr_ratiolimit) ratioSetting
394     return tr_torrentGetRatioMode(fHandle);
397 - (void) setRatioSetting: (tr_ratiolimit) setting
399     tr_torrentSetRatioMode(fHandle, setting);
402 - (CGFloat) ratioLimit
404     return tr_torrentGetRatioLimit(fHandle);
407 - (void) setRatioLimit: (CGFloat) limit
409     NSAssert(limit >= 0, @"Ratio cannot be negative");
410     tr_torrentSetRatioLimit(fHandle, limit);
413 - (CGFloat) progressStopRatio
415     return fStat->seedRatioPercentDone;
418 - (tr_idlelimit) idleSetting
420     return tr_torrentGetIdleMode(fHandle);
423 - (void) setIdleSetting: (tr_idlelimit) setting
425     tr_torrentSetIdleMode(fHandle, setting);
428 - (NSUInteger) idleLimitMinutes
430     return tr_torrentGetIdleLimit(fHandle);
433 - (void) setIdleLimitMinutes: (NSUInteger) limit
435     NSAssert(limit > 0, @"Idle limit must be greater than zero");
436     tr_torrentSetIdleLimit(fHandle, limit);
439 - (BOOL) usesSpeedLimit: (BOOL) upload
441     return tr_torrentUsesSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN);
444 - (void) setUseSpeedLimit: (BOOL) use upload: (BOOL) upload
446     tr_torrentUseSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN, use);
449 - (NSInteger) speedLimit: (BOOL) upload
451     return tr_torrentGetSpeedLimit_KBps(fHandle, upload ? TR_UP : TR_DOWN);
454 - (void) setSpeedLimit: (NSInteger) limit upload: (BOOL) upload
456     tr_torrentSetSpeedLimit_KBps(fHandle, upload ? TR_UP : TR_DOWN, limit);
459 - (BOOL) usesGlobalSpeedLimit
461     return tr_torrentUsesSessionLimits(fHandle);
464 - (void) setUseGlobalSpeedLimit: (BOOL) use
466     tr_torrentUseSessionLimits(fHandle, use);
469 - (void) setMaxPeerConnect: (uint16_t) count
471     NSAssert(count > 0, @"max peer count must be greater than 0");
472     
473     tr_torrentSetPeerLimit(fHandle, count);
476 - (uint16_t) maxPeerConnect
478     return tr_torrentGetPeerLimit(fHandle);
480 - (BOOL) waitingToStart
482     return fStat->activity == TR_STATUS_DOWNLOAD_WAIT || fStat->activity == TR_STATUS_SEED_WAIT;
485 - (tr_priority_t) priority
487     return tr_torrentGetPriority(fHandle);
490 - (void) setPriority: (tr_priority_t) priority
492     return tr_torrentSetPriority(fHandle, priority);
495 #warning when 10.6-only use recycleURLs:completionHandler:
496 + (void) trashFile: (NSString *) path
498     //attempt to move to trash
499     if (![[NSWorkspace sharedWorkspace] performFileOperation: NSWorkspaceRecycleOperation
500         source: [path stringByDeletingLastPathComponent] destination: @""
501         files: [NSArray arrayWithObject: [path lastPathComponent]] tag: nil])
502     {
503         //if cannot trash, just delete it (will work if it's on a remote volume)
504         NSError * error;
505         if (![[NSFileManager defaultManager] removeItemAtPath: path error: &error])
506             NSLog(@"Could not trash %@: %@", path, [error localizedDescription]);
507     }
510 - (void) moveTorrentDataFileTo: (NSString *) folder
512     NSString * oldFolder = [self currentDirectory];
513     if ([oldFolder isEqualToString: folder])
514         return;
515     
516     //check if moving inside itself
517     NSArray * oldComponents = [oldFolder pathComponents],
518             * newComponents = [folder pathComponents];
519     const NSInteger oldCount = [oldComponents count];
520     
521     if (oldCount < [newComponents count] && [[newComponents objectAtIndex: oldCount] isEqualToString: [self name]]
522         && [folder hasPrefix: oldFolder])
523     {
524         NSAlert * alert = [[NSAlert alloc] init];
525         [alert setMessageText: NSLocalizedString(@"A folder cannot be moved to inside itself.",
526                                                     "Move inside itself alert -> title")];
527         [alert setInformativeText: [NSString stringWithFormat:
528                         NSLocalizedString(@"The move operation of \"%@\" cannot be done.",
529                                             "Move inside itself alert -> message"), [self name]]];
530         [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move inside itself alert -> button")];
531         
532         [alert runModal];
533         [alert release];
534         
535         return;
536     }
537     
538     volatile int status;
539     tr_torrentSetLocation(fHandle, [folder UTF8String], YES, NULL, &status);
540     
541     while (status == TR_LOC_MOVING) //block while moving (for now)
542         [NSThread sleepForTimeInterval: 0.05];
543     
544     if (status == TR_LOC_DONE)
545         [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateStats" object: nil];
546     else
547     {
548         NSAlert * alert = [[NSAlert alloc] init];
549         [alert setMessageText: NSLocalizedString(@"There was an error moving the data file.", "Move error alert -> title")];
550         [alert setInformativeText: [NSString stringWithFormat:
551                 NSLocalizedString(@"The move operation of \"%@\" cannot be done.", "Move error alert -> message"), [self name]]];
552         [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move error alert -> button")];
553         
554         [alert runModal];
555         [alert release];
556     }
557     
558     [self updateTimeMachineExclude];
561 - (void) copyTorrentFileTo: (NSString *) path
563     [[NSFileManager defaultManager] copyItemAtPath: [self torrentLocation] toPath: path error: NULL];
566 - (BOOL) alertForRemainingDiskSpace
568     if ([self allDownloaded] || ![fDefaults boolForKey: @"WarningRemainingSpace"])
569         return YES;
570     
571     NSString * downloadFolder = [self currentDirectory];
572     NSDictionary * systemAttributes;
573     if ((systemAttributes = [[NSFileManager defaultManager] attributesOfFileSystemForPath: downloadFolder error: NULL]))
574     {
575         const uint64_t remainingSpace = [[systemAttributes objectForKey: NSFileSystemFreeSize] unsignedLongLongValue];
576         
577         //if the remaining space is greater than the size left, then there is enough space regardless of preallocation
578         if (remainingSpace < [self sizeLeft] && remainingSpace < tr_torrentGetBytesLeftToAllocate(fHandle))
579         {
580             NSString * volumeName = [[[NSFileManager defaultManager] componentsToDisplayForPath: downloadFolder] objectAtIndex: 0];
581             
582             NSAlert * alert = [[NSAlert alloc] init];
583             [alert setMessageText: [NSString stringWithFormat:
584                                     NSLocalizedString(@"Not enough remaining disk space to download \"%@\" completely.",
585                                         "Torrent disk space alert -> title"), [self name]]];
586             [alert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"The transfer will be paused."
587                                         " Clear up space on %@ or deselect files in the torrent inspector to continue.",
588                                         "Torrent disk space alert -> message"), volumeName]];
589             [alert addButtonWithTitle: NSLocalizedString(@"OK", "Torrent disk space alert -> button")];
590             [alert addButtonWithTitle: NSLocalizedString(@"Download Anyway", "Torrent disk space alert -> button")];
591             
592             [alert setShowsSuppressionButton: YES];
593             [[alert suppressionButton] setTitle: NSLocalizedString(@"Do not check disk space again",
594                                                     "Torrent disk space alert -> button")];
596             const NSInteger result = [alert runModal];
597             if ([[alert suppressionButton] state] == NSOnState)
598                 [fDefaults setBool: NO forKey: @"WarningRemainingSpace"];
599             [alert release];
600             
601             return result != NSAlertFirstButtonReturn;
602         }
603     }
604     return YES;
607 - (NSImage *) icon
609     if ([self isMagnet])
610         return [NSImage imageNamed: @"Magnet.png"];
611     
612     #warning replace kGenericFolderIcon stuff with NSImageNameFolder on 10.6
613     if (!fIcon)
614         fIcon = [[[NSWorkspace sharedWorkspace] iconForFileType: [self isFolder] ? NSFileTypeForHFSTypeCode(kGenericFolderIcon)
615                                                                                 : [[self name] pathExtension]] retain];
616     return fIcon;
619 - (NSString *) name
621     return fInfo->name != NULL ? [NSString stringWithUTF8String: fInfo->name] : fHashString;
624 - (BOOL) isFolder
626     return fInfo->isMultifile;
629 - (uint64_t) size
631     return fInfo->totalSize;
634 - (uint64_t) sizeLeft
636     return fStat->leftUntilDone;
639 - (NSMutableArray *) allTrackerStats
641     int count;
642     tr_tracker_stat * stats = tr_torrentTrackers(fHandle, &count);
643     
644     NSMutableArray * trackers = [NSMutableArray arrayWithCapacity: (count > 0 ? count + (stats[count-1].tier + 1) : 0)];
645     
646     int prevTier = -1;
647     for (int i=0; i < count; ++i)
648     {
649         if (stats[i].tier != prevTier)
650         {
651             [trackers addObject: [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInteger: stats[i].tier + 1], @"Tier",
652                                     [self name], @"Name", nil]];
653             prevTier = stats[i].tier;
654         }
655         
656         TrackerNode * tracker = [[TrackerNode alloc] initWithTrackerStat: &stats[i] torrent: self];
657         [trackers addObject: tracker];
658         [tracker release];
659     }
660     
661     tr_torrentTrackersFree(stats, count);
662     return trackers;
665 - (NSArray *) allTrackersFlat
667     NSMutableArray * allTrackers = [NSMutableArray arrayWithCapacity: fInfo->trackerCount];
668     
669     for (NSInteger i=0; i < fInfo->trackerCount; i++)
670         [allTrackers addObject: [NSString stringWithUTF8String: fInfo->trackers[i].announce]];
671     
672     return allTrackers;
675 - (BOOL) addTrackerToNewTier: (NSString *) tracker
677     tracker = [tracker stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
678     
679     if ([tracker rangeOfString: @"://"].location == NSNotFound)
680         tracker = [@"http://" stringByAppendingString: tracker];
681     
682     //recreate the tracker structure
683     const int oldTrackerCount = fInfo->trackerCount;
684     tr_tracker_info * trackerStructs = tr_new(tr_tracker_info, oldTrackerCount+1);
685     for (NSUInteger i=0; i < oldTrackerCount; ++i)
686         trackerStructs[i] = fInfo->trackers[i];
687     
688     trackerStructs[oldTrackerCount].announce = (char *)[tracker UTF8String];
689     trackerStructs[oldTrackerCount].tier = trackerStructs[oldTrackerCount-1].tier + 1;
690     trackerStructs[oldTrackerCount].id = oldTrackerCount;
691     
692     const BOOL success = tr_torrentSetAnnounceList(fHandle, trackerStructs, oldTrackerCount+1);
693     tr_free(trackerStructs);
694     
695     return success;
698 - (void) removeTrackers: (NSSet *) trackers
700     //recreate the tracker structure
701     tr_tracker_info * trackerStructs = tr_new(tr_tracker_info, fInfo->trackerCount);
702     
703     NSUInteger newCount = 0;
704     for (NSUInteger i = 0; i < fInfo->trackerCount; i++)
705     {
706         if (![trackers containsObject: [NSString stringWithUTF8String: fInfo->trackers[i].announce]])
707             trackerStructs[newCount++] = fInfo->trackers[i];
708     }
709     
710     const BOOL success = tr_torrentSetAnnounceList(fHandle, trackerStructs, newCount);
711     NSAssert(success, @"Removing tracker addresses failed");
712     
713     tr_free(trackerStructs);
716 - (NSString *) comment
718     return fInfo->comment ? [NSString stringWithUTF8String: fInfo->comment] : @"";
721 - (NSString *) creator
723     return fInfo->creator ? [NSString stringWithUTF8String: fInfo->creator] : @"";
726 - (NSDate *) dateCreated
728     NSInteger date = fInfo->dateCreated;
729     return date > 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
732 - (NSInteger) pieceSize
734     return fInfo->pieceSize;
737 - (NSInteger) pieceCount
739     return fInfo->pieceCount;
742 - (NSString *) hashString
744     return fHashString;
747 - (BOOL) privateTorrent
749     return fInfo->isPrivate;
752 - (NSString *) torrentLocation
754     return fInfo->torrent ? [NSString stringWithUTF8String: fInfo->torrent] : @"";
757 - (NSString *) dataLocation
759     if ([self isMagnet])
760         return nil;
761     
762     if ([self isFolder])
763     {
764         NSString * dataLocation = [[self currentDirectory] stringByAppendingPathComponent: [self name]];
765         
766         if (![[NSFileManager defaultManager] fileExistsAtPath: dataLocation])
767             return nil;
768         
769         return dataLocation;
770     }
771     else
772     {
773         char * location = tr_torrentFindFile(fHandle, 0);
774         if (location == NULL)
775             return nil;
776         
777         NSString * dataLocation = [NSString stringWithUTF8String: location];
778         free(location);
779         
780         return dataLocation;
781     }
784 - (NSString *) fileLocation: (FileListNode *) node
786     if ([node isFolder])
787     {
788         NSString * basePath = [[node path] stringByAppendingPathComponent: [node name]];
789         NSString * dataLocation = [[self currentDirectory] stringByAppendingPathComponent: basePath];
790         
791         if (![[NSFileManager defaultManager] fileExistsAtPath: dataLocation])
792             return nil;
793         
794         return dataLocation;
795     }
796     else
797     {
798         char * location = tr_torrentFindFile(fHandle, [[node indexes] firstIndex]);
799         if (location == NULL)
800             return nil;
801         
802         NSString * dataLocation = [NSString stringWithUTF8String: location];
803         free(location);
804         
805         return dataLocation;
806     }
809 - (CGFloat) progress
811     return fStat->percentComplete;
814 - (CGFloat) progressDone
816     return fStat->percentDone;
819 - (CGFloat) progressLeft
821     if ([self size] == 0) //magnet links
822         return 0.0;
823     
824     return (CGFloat)[self sizeLeft] / [self size];
827 - (CGFloat) checkingProgress
829     return fStat->recheckProgress;
832 - (CGFloat) availableDesired
834     return (CGFloat)fStat->desiredAvailable / [self sizeLeft];
837 - (BOOL) isActive
839     return fStat->activity != TR_STATUS_STOPPED && fStat->activity != TR_STATUS_DOWNLOAD_WAIT && fStat->activity != TR_STATUS_SEED_WAIT;
842 - (BOOL) isSeeding
844     return fStat->activity == TR_STATUS_SEED;
847 - (BOOL) isChecking
849     return fStat->activity == TR_STATUS_CHECK || fStat->activity == TR_STATUS_CHECK_WAIT;
852 - (BOOL) isCheckingWaiting
854     return fStat->activity == TR_STATUS_CHECK_WAIT;
857 - (BOOL) allDownloaded
859     return [self sizeLeft] == 0 && ![self isMagnet];
862 - (BOOL) isComplete
864     return [self progress] >= 1.0;
867 - (BOOL) isFinishedSeeding
869     return fStat->finished;
872 - (BOOL) isError
874     return fStat->error == TR_STAT_LOCAL_ERROR;
877 - (BOOL) isAnyErrorOrWarning
879     return fStat->error != TR_STAT_OK;
882 - (NSString *) errorMessage
884     if (![self isAnyErrorOrWarning])
885         return @"";
886     
887     NSString * error;
888     if (!(error = [NSString stringWithUTF8String: fStat->errorString])
889         && !(error = [NSString stringWithCString: fStat->errorString encoding: NSISOLatin1StringEncoding]))
890         error = [NSString stringWithFormat: @"(%@)", NSLocalizedString(@"unreadable error", "Torrent -> error string unreadable")];
891     
892     //libtransmission uses "Set Location", Mac client uses "Move data file to..." - very hacky!
893     error = [error stringByReplacingOccurrencesOfString: @"Set Location" withString: [@"Move Data File To" stringByAppendingEllipsis]];
894     
895     return error;
898 - (NSArray *) peers
900     int totalPeers;
901     tr_peer_stat * peers = tr_torrentPeers(fHandle, &totalPeers);
902     
903     NSMutableArray * peerDicts = [NSMutableArray arrayWithCapacity: totalPeers];
904     
905     for (int i = 0; i < totalPeers; i++)
906     {
907         tr_peer_stat * peer = &peers[i];
908         NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity: 12];
909         
910         [dict setObject: [self name] forKey: @"Name"];
911         [dict setObject: [NSNumber numberWithInt: peer->from] forKey: @"From"];
912         [dict setObject: [NSString stringWithUTF8String: peer->addr] forKey: @"IP"];
913         [dict setObject: [NSNumber numberWithInt: peer->port] forKey: @"Port"];
914         [dict setObject: [NSNumber numberWithFloat: peer->progress] forKey: @"Progress"];
915         [dict setObject: [NSNumber numberWithBool: peer->isSeed] forKey: @"Seed"];
916         [dict setObject: [NSNumber numberWithBool: peer->isEncrypted] forKey: @"Encryption"];
917         [dict setObject: [NSNumber numberWithBool: peer->isUTP] forKey: @"uTP"];
918         [dict setObject: [NSString stringWithUTF8String: peer->client] forKey: @"Client"];
919         [dict setObject: [NSString stringWithUTF8String: peer->flagStr] forKey: @"Flags"];
920         
921         if (peer->isUploadingTo)
922             [dict setObject: [NSNumber numberWithDouble: peer->rateToPeer_KBps] forKey: @"UL To Rate"];
923         if (peer->isDownloadingFrom)
924             [dict setObject: [NSNumber numberWithDouble: peer->rateToClient_KBps] forKey: @"DL From Rate"];
925         
926         [peerDicts addObject: dict];
927     }
928     
929     tr_torrentPeersFree(peers, totalPeers);
930     
931     return peerDicts;
934 - (NSUInteger) webSeedCount
936     return fInfo->webseedCount;
939 - (NSArray *) webSeeds
941     NSMutableArray * webSeeds = [NSMutableArray arrayWithCapacity: fInfo->webseedCount];
942     
943     double * dlSpeeds = tr_torrentWebSpeeds_KBps(fHandle);
944     
945     for (NSInteger i = 0; i < fInfo->webseedCount; i++)
946     {
947         NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity: 3];
948         
949         [dict setObject: [self name] forKey: @"Name"];
950         [dict setObject: [NSString stringWithUTF8String: fInfo->webseeds[i]] forKey: @"Address"];
951         
952         if (dlSpeeds[i] != -1.0)
953             [dict setObject: [NSNumber numberWithDouble: dlSpeeds[i]] forKey: @"DL From Rate"];
954         
955         [webSeeds addObject: dict];
956     }
957     
958     tr_free(dlSpeeds);
959     
960     return webSeeds;
963 - (NSString *) progressString
965     if ([self isMagnet])
966     {
967         NSString * progressString = fStat->metadataPercentComplete > 0.0
968                     ? [NSString stringWithFormat: NSLocalizedString(@"%@ of torrent metadata retrieved",
969                         "Torrent -> progress string"), [NSString percentString: fStat->metadataPercentComplete longDecimals: YES]]
970                     : NSLocalizedString(@"torrent metadata needed", "Torrent -> progress string");
971         
972         return [NSString stringWithFormat: @"%@ - %@", NSLocalizedString(@"Magnetized transfer", "Torrent -> progress string"),
973                                             progressString];
974     }
975     
976     NSString * string;
977     
978     if (![self allDownloaded])
979     {
980         CGFloat progress;
981         if ([self isFolder] && [fDefaults boolForKey: @"DisplayStatusProgressSelected"])
982         {
983             string = [NSString stringForFilePartialSize: [self haveTotal] fullSize: [self totalSizeSelected]];
984             progress = [self progressDone];
985         }
986         else
987         {
988             string = [NSString stringForFilePartialSize: [self haveTotal] fullSize: [self size]];
989             progress = [self progress];
990         }
991         
992         string = [string stringByAppendingFormat: @" (%@)", [NSString percentString: progress longDecimals: YES]];
993     }
994     else
995     {
996         NSString * downloadString;
997         if (![self isComplete]) //only multifile possible
998         {
999             if ([fDefaults boolForKey: @"DisplayStatusProgressSelected"])
1000                 downloadString = [NSString stringWithFormat: NSLocalizedString(@"%@ selected", "Torrent -> progress string"),
1001                                     [NSString stringForFileSize: [self haveTotal]]];
1002             else
1003             {
1004                 downloadString = [NSString stringForFilePartialSize: [self haveTotal] fullSize: [self size]];
1005                 downloadString = [downloadString stringByAppendingFormat: @" (%@)",
1006                                     [NSString percentString: [self progress] longDecimals: YES]];
1007             }
1008         }
1009         else
1010             downloadString = [NSString stringForFileSize: [self size]];
1011         
1012         NSString * uploadString = [NSString stringWithFormat: NSLocalizedString(@"uploaded %@ (Ratio: %@)",
1013                                     "Torrent -> progress string"), [NSString stringForFileSize: [self uploadedTotal]],
1014                                     [NSString stringForRatio: [self ratio]]];
1015         
1016         string = [downloadString stringByAppendingFormat: @", %@", uploadString];
1017     }
1018     
1019     //add time when downloading or seed limit set
1020     if ([self shouldShowEta])
1021         string = [string stringByAppendingFormat: @" - %@", [self etaString]];
1022     
1023     return string;
1026 - (NSString *) statusString
1028     NSString * string;
1029     
1030     if ([self isAnyErrorOrWarning])
1031     {
1032         switch (fStat->error)
1033         {
1034             case TR_STAT_LOCAL_ERROR: string = NSLocalizedString(@"Error", "Torrent -> status string"); break;
1035             case TR_STAT_TRACKER_ERROR: string = NSLocalizedString(@"Tracker returned error", "Torrent -> status string"); break;
1036             case TR_STAT_TRACKER_WARNING: string = NSLocalizedString(@"Tracker returned warning", "Torrent -> status string"); break;
1037             default: NSAssert(NO, @"unknown error state");
1038         }
1039         
1040         NSString * errorString = [self errorMessage];
1041         if (errorString && ![errorString isEqualToString: @""])
1042             string = [string stringByAppendingFormat: @": %@", errorString];
1043     }
1044     else
1045     {
1046         switch (fStat->activity)
1047         {
1048             case TR_STATUS_STOPPED:
1049                 if ([self isFinishedSeeding])
1050                     string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1051                 else
1052                     string = NSLocalizedString(@"Paused", "Torrent -> status string");
1053                 break;
1054             
1055             case TR_STATUS_DOWNLOAD_WAIT:
1056                 string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis];
1057                 break;
1058                 
1059             case TR_STATUS_SEED_WAIT:
1060                 string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
1061                 break;
1062             
1063             case TR_STATUS_CHECK_WAIT:
1064                 string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1065                 break;
1067             case TR_STATUS_CHECK:
1068                 string = [NSString stringWithFormat: @"%@ (%@)",
1069                             NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
1070                             [NSString percentString: [self checkingProgress] longDecimals: YES]];
1071                 break;
1073             case TR_STATUS_DOWNLOAD:
1074                 if ([self totalPeersConnected] != 1)
1075                     string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of %d peers",
1076                                                     "Torrent -> status string"), [self peersSendingToUs], [self totalPeersConnected]];
1077                 else
1078                     string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of 1 peer",
1079                                                     "Torrent -> status string"), [self peersSendingToUs]];
1080                 
1081                 const NSInteger webSeedCount = fStat->webseedsSendingToUs;
1082                 if (webSeedCount > 0)
1083                 {
1084                     NSString * webSeedString;
1085                     if (webSeedCount == 1)
1086                         webSeedString = NSLocalizedString(@"web seed", "Torrent -> status string");
1087                     else
1088                         webSeedString = [NSString stringWithFormat: NSLocalizedString(@"%d web seeds", "Torrent -> status string"),
1089                                                                     webSeedCount];
1090                     
1091                     string = [string stringByAppendingFormat: @" + %@", webSeedString];
1092                 }
1093                 
1094                 break;
1096             case TR_STATUS_SEED:
1097                 if ([self totalPeersConnected] != 1)
1098                     string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of %d peers", "Torrent -> status string"),
1099                                                     [self peersGettingFromUs], [self totalPeersConnected]];
1100                 else
1101                     string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of 1 peer", "Torrent -> status string"),
1102                                                     [self peersGettingFromUs]];
1103         }
1104         
1105         if ([self isStalled])
1106             string = [NSLocalizedString(@"Stalled", "Torrent -> status string") stringByAppendingFormat: @", %@", string];
1107     }
1108     
1109     //append even if error
1110     if ([self isActive] && ![self isChecking])
1111     {
1112         if (fStat->activity == TR_STATUS_DOWNLOAD)
1113             string = [string stringByAppendingFormat: @" - %@: %@, %@: %@",
1114                         NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]],
1115                         NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1116         else
1117             string = [string stringByAppendingFormat: @" - %@: %@",
1118                         NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1119     }
1120     
1121     return string;
1124 - (NSString *) shortStatusString
1126     NSString * string;
1127     
1128     switch (fStat->activity)
1129     {
1130         case TR_STATUS_STOPPED:
1131             if ([self isFinishedSeeding])
1132                 string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1133             else
1134                 string = NSLocalizedString(@"Paused", "Torrent -> status string");
1135             break;
1136         
1137         case TR_STATUS_DOWNLOAD_WAIT:
1138             string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis];
1139             break;
1140             
1141         case TR_STATUS_SEED_WAIT:
1142             string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
1143             break;
1145         case TR_STATUS_CHECK_WAIT:
1146             string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1147             break;
1149         case TR_STATUS_CHECK:
1150             string = [NSString stringWithFormat: @"%@ (%@)",
1151                         NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
1152                         [NSString percentString: [self checkingProgress] longDecimals: YES]];
1153             break;
1154         
1155         case TR_STATUS_DOWNLOAD:
1156             string = [NSString stringWithFormat: @"%@: %@, %@: %@",
1157                             NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]],
1158                             NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1159             break;
1160         
1161         case TR_STATUS_SEED:
1162             string = [NSString stringWithFormat: @"%@: %@, %@: %@",
1163                             NSLocalizedString(@"Ratio", "Torrent -> status string"), [NSString stringForRatio: [self ratio]],
1164                             NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1165     }
1166     
1167     return string;
1170 - (NSString *) remainingTimeString
1172     if ([self shouldShowEta])
1173         return [self etaString];
1174     else
1175         return [self shortStatusString];
1178 - (NSString *) stateString
1180     switch (fStat->activity)
1181     {
1182         case TR_STATUS_STOPPED:
1183         case TR_STATUS_DOWNLOAD_WAIT:
1184         case TR_STATUS_SEED_WAIT:
1185         {
1186             NSString * string = NSLocalizedString(@"Paused", "Torrent -> status string");
1187             
1188             NSString * extra = nil;
1189             if ([self waitingToStart])
1190             {
1191                 extra = fStat->activity == TR_STATUS_DOWNLOAD_WAIT 
1192                         ? NSLocalizedString(@"Waiting to download", "Torrent -> status string")
1193                         : NSLocalizedString(@"Waiting to seed", "Torrent -> status string");
1194             }
1195             else if ([self isFinishedSeeding])
1196                 extra = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1197             else;
1198         
1199             return extra ? [string stringByAppendingFormat: @" (%@)", extra] : string;
1200         }
1201         
1202         case TR_STATUS_CHECK_WAIT:
1203             return [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1205         case TR_STATUS_CHECK:
1206             return [NSString stringWithFormat: @"%@ (%@)",
1207                     NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
1208                     [NSString percentString: [self checkingProgress] longDecimals: YES]];
1210         case TR_STATUS_DOWNLOAD:
1211             return NSLocalizedString(@"Downloading", "Torrent -> status string");
1213         case TR_STATUS_SEED:
1214             return NSLocalizedString(@"Seeding", "Torrent -> status string");
1215     }
1218 - (NSInteger) totalPeersConnected
1220     return fStat->peersConnected;
1223 - (NSInteger) totalPeersTracker
1225     return fStat->peersFrom[TR_PEER_FROM_TRACKER];
1228 - (NSInteger) totalPeersIncoming
1230     return fStat->peersFrom[TR_PEER_FROM_INCOMING];
1233 - (NSInteger) totalPeersCache
1235     return fStat->peersFrom[TR_PEER_FROM_RESUME];
1238 - (NSInteger) totalPeersPex
1240     return fStat->peersFrom[TR_PEER_FROM_PEX];
1243 - (NSInteger) totalPeersDHT
1245     return fStat->peersFrom[TR_PEER_FROM_DHT];
1248 - (NSInteger) totalPeersLocal
1250     return fStat->peersFrom[TR_PEER_FROM_LPD];
1253 - (NSInteger) totalPeersLTEP
1255     return fStat->peersFrom[TR_PEER_FROM_LTEP];
1258 - (NSInteger) peersSendingToUs
1260     return fStat->peersSendingToUs;
1263 - (NSInteger) peersGettingFromUs
1265     return fStat->peersGettingFromUs;
1268 - (CGFloat) downloadRate
1270     return fStat->pieceDownloadSpeed_KBps;
1273 - (CGFloat) uploadRate
1275     return fStat->pieceUploadSpeed_KBps;
1278 - (CGFloat) totalRate
1280     return [self downloadRate] + [self uploadRate];
1283 - (uint64_t) haveVerified
1285     return fStat->haveValid;
1288 - (uint64_t) haveTotal
1290     return [self haveVerified] + fStat->haveUnchecked;
1293 - (uint64_t) totalSizeSelected
1295     return fStat->sizeWhenDone;
1298 - (uint64_t) downloadedTotal
1300     return fStat->downloadedEver;
1303 - (uint64_t) uploadedTotal
1305     return fStat->uploadedEver;
1308 - (uint64_t) failedHash
1310     return fStat->corruptEver;
1313 - (NSInteger) groupValue
1315     return fGroupValue;
1318 - (void) setGroupValue: (NSInteger) goupValue
1320     fGroupValue = goupValue;
1323 - (NSInteger) groupOrderValue
1325     return [[GroupsController groups] rowValueForIndex: fGroupValue];
1328 - (void) checkGroupValueForRemoval: (NSNotification *) notification
1330     if (fGroupValue != -1 && [[[notification userInfo] objectForKey: @"Index"] integerValue] == fGroupValue)
1331         fGroupValue = -1;
1334 - (NSArray *) fileList
1336     return fFileList;
1339 - (NSArray *) flatFileList
1341     return fFlatFileList;
1344 - (NSInteger) fileCount
1346     return fInfo->fileCount;
1349 - (void) updateFileStat
1351     if (fFileStat)
1352         tr_torrentFilesFree(fFileStat, [self fileCount]);
1353     
1354     fFileStat = tr_torrentFiles(fHandle, NULL);
1357 - (CGFloat) fileProgress: (FileListNode *) node
1359     if ([self fileCount] == 1 || [self isComplete])
1360         return [self progress];
1361     
1362     if (!fFileStat)
1363         [self updateFileStat];
1364     
1365     NSIndexSet * indexSet = [node indexes];
1366     
1367     if ([indexSet count] == 1)
1368         return fFileStat[[indexSet firstIndex]].progress;
1369     
1370     uint64_t have = 0;
1371     for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1372         have += fFileStat[index].bytesCompleted;
1373     
1374     NSAssert([node size], @"directory in torrent file has size 0");
1375     return (CGFloat)have / [node size];
1378 - (BOOL) canChangeDownloadCheckForFile: (NSUInteger) index
1380     NSAssert2(index < [self fileCount], @"Index %d is greater than file count %d", index, [self fileCount]);
1381     
1382     return [self canChangeDownloadCheckForFiles: [NSIndexSet indexSetWithIndex: index]];
1385 - (BOOL) canChangeDownloadCheckForFiles: (NSIndexSet *) indexSet
1387     if ([self fileCount] == 1 || [self isComplete])
1388         return NO;
1389     
1390     if (!fFileStat)
1391         [self updateFileStat];
1392     
1393     __block BOOL canChange = NO;
1394     [indexSet enumerateIndexesWithOptions: NSEnumerationConcurrent usingBlock: ^(NSUInteger index, BOOL *stop) {
1395         if (fFileStat[index].progress < 1.0)
1396         {
1397             canChange = YES;
1398             *stop = YES;
1399         }
1400     }];
1401     return canChange;
1404 - (NSInteger) checkForFiles: (NSIndexSet *) indexSet
1406     BOOL onState = NO, offState = NO;
1407     for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1408     {
1409         if (!fInfo->files[index].dnd || ![self canChangeDownloadCheckForFile: index])
1410             onState = YES;
1411         else
1412             offState = YES;
1413         
1414         if (onState && offState)
1415             return NSMixedState;
1416     }
1417     return onState ? NSOnState : NSOffState;
1420 - (void) setFileCheckState: (NSInteger) state forIndexes: (NSIndexSet *) indexSet
1422     NSUInteger count = [indexSet count];
1423     tr_file_index_t * files = malloc(count * sizeof(tr_file_index_t));
1424     for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++)
1425         files[i] = index;
1426     
1427     tr_torrentSetFileDLs(fHandle, files, count, state != NSOffState);
1428     free(files);
1429     
1430     [self update];
1431     [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFileCheckChange" object: self];
1434 - (void) setFilePriority: (tr_priority_t) priority forIndexes: (NSIndexSet *) indexSet
1436     const NSUInteger count = [indexSet count];
1437     tr_file_index_t * files = tr_malloc(count * sizeof(tr_file_index_t));
1438     for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++)
1439         files[i] = index;
1440     
1441     tr_torrentSetFilePriorities(fHandle, files, count, priority);
1442     tr_free(files);
1445 - (BOOL) hasFilePriority: (tr_priority_t) priority forIndexes: (NSIndexSet *) indexSet
1447     for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1448         if (priority == fInfo->files[index].priority && [self canChangeDownloadCheckForFile: index])
1449             return YES;
1450     return NO;
1453 - (NSSet *) filePrioritiesForIndexes: (NSIndexSet *) indexSet
1455     BOOL low = NO, normal = NO, high = NO;
1456     NSMutableSet * priorities = [NSMutableSet setWithCapacity: MIN([indexSet count], 3)];
1457     
1458     for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1459     {
1460         if (![self canChangeDownloadCheckForFile: index])
1461             continue;
1462         
1463         const tr_priority_t priority = fInfo->files[index].priority;
1464         switch (priority)
1465         {
1466             case TR_PRI_LOW:
1467                 if (low)
1468                     continue;
1469                 low = YES;
1470                 break;
1471             case TR_PRI_NORMAL:
1472                 if (normal)
1473                     continue;
1474                 normal = YES;
1475                 break;
1476             case TR_PRI_HIGH:
1477                 if (high)
1478                     continue;
1479                 high = YES;
1480                 break;
1481             default:
1482                 NSAssert2(NO, @"Unknown priority %d for file index %d", priority, index);
1483         }
1484         
1485         [priorities addObject: [NSNumber numberWithInteger: priority]];
1486         if (low && normal && high)
1487             break;
1488     }
1489     return priorities;
1492 - (NSDate *) dateAdded
1494     const time_t date = fStat->addedDate;
1495     return [NSDate dateWithTimeIntervalSince1970: date];
1498 - (NSDate *) dateCompleted
1500     const time_t date = fStat->doneDate;
1501     return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
1504 - (NSDate *) dateActivity
1506     const time_t date = fStat->activityDate;
1507     return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
1510 - (NSDate *) dateActivityOrAdd
1512     NSDate * date = [self dateActivity];
1513     return date ? date : [self dateAdded];
1516 - (NSInteger) secondsDownloading
1518     return fStat->secondsDownloading;
1521 - (NSInteger) secondsSeeding
1523     return fStat->secondsSeeding;
1526 - (NSInteger) stalledMinutes
1528     if (fStat->idleSecs == -1)
1529         return -1;
1530     
1531     return fStat->idleSecs / 60;
1534 - (BOOL) isStalled
1536     return fStat->isStalled;
1539 - (void) updateTimeMachineExclude
1541     NSString * currentLocation = ![self allDownloaded] ? [self dataLocation] : nil;
1542     
1543     //return if the locations are the same
1544     if (fTimeMachineExclude && currentLocation && [fTimeMachineExclude isEqualToString: currentLocation])
1545         return;
1546     
1547     //remove old location...
1548     if (fTimeMachineExclude)
1549     {
1550         [self setTimeMachineExclude: NO forPath: fTimeMachineExclude];
1551         [fTimeMachineExclude release];
1552         fTimeMachineExclude = nil;
1553     }
1554     
1555     //...set new location
1556     if (currentLocation)
1557     {
1558         [self setTimeMachineExclude: YES forPath: currentLocation];
1559         fTimeMachineExclude = [currentLocation retain];
1560     }
1563 - (NSInteger) stateSortKey
1565     if (![self isActive]) //paused
1566     {
1567         if ([self waitingToStart])
1568             return 1;
1569         else
1570             return 0;
1571     }
1572     else if ([self isSeeding]) //seeding
1573         return 10;
1574     else //downloading
1575         return 20;
1578 - (NSString *) trackerSortKey
1580     int count;
1581     tr_tracker_stat * stats = tr_torrentTrackers(fHandle, &count);
1582     
1583     NSString * best = nil;
1584     
1585     for (int i=0; i < count; ++i)
1586     {
1587         NSString * tracker = [NSString stringWithUTF8String: stats[i].host];
1588         if (!best || [tracker localizedCaseInsensitiveCompare: best] == NSOrderedAscending)
1589             best = tracker;
1590     }
1591     
1592     tr_torrentTrackersFree(stats, count);
1593     return best;
1596 - (tr_torrent *) torrentStruct
1598     return fHandle;
1601 - (NSURL *) previewItemURL
1603     NSString * location = [self dataLocation];
1604     return location ? [NSURL fileURLWithPath: location] : nil;
1607 @end
1609 @implementation Torrent (Private)
1611 - (id) initWithPath: (NSString *) path hash: (NSString *) hashString torrentStruct: (tr_torrent *) torrentStruct
1612         magnetAddress: (NSString *) magnetAddress lib: (tr_session *) lib
1613         groupValue: (NSNumber *) groupValue
1614         timeMachineExcludeLocation: (NSString *) timeMachineExclude
1615         downloadFolder: (NSString *) downloadFolder
1616         legacyIncompleteFolder: (NSString *) incompleteFolder
1618     if (!(self = [super init]))
1619         return nil;
1620     
1621     fDefaults = [NSUserDefaults standardUserDefaults];
1622     
1623     if (torrentStruct)
1624         fHandle = torrentStruct;
1625     else
1626     {
1627         //set libtransmission settings for initialization
1628         tr_ctor * ctor = tr_ctorNew(lib);
1629         
1630         tr_ctorSetPaused(ctor, TR_FORCE, YES);
1631         if (downloadFolder)
1632             tr_ctorSetDownloadDir(ctor, TR_FORCE, [downloadFolder UTF8String]);
1633         if (incompleteFolder)
1634             tr_ctorSetIncompleteDir(ctor, [incompleteFolder UTF8String]);
1635         
1636         tr_parse_result result = TR_PARSE_ERR;
1637         if (path)
1638             result = tr_ctorSetMetainfoFromFile(ctor, [path UTF8String]);
1639         
1640         if (result != TR_PARSE_OK && magnetAddress)
1641             result = tr_ctorSetMetainfoFromMagnetLink(ctor, [magnetAddress UTF8String]);
1642         
1643         //backup - shouldn't be needed after upgrade to 1.70
1644         if (result != TR_PARSE_OK && hashString)
1645             result = tr_ctorSetMetainfoFromHash(ctor, [hashString UTF8String]);
1646         
1647         if (result == TR_PARSE_OK)
1648             fHandle = tr_torrentNew(ctor, NULL);
1649         
1650         tr_ctorFree(ctor);
1651         
1652         if (!fHandle)
1653         {
1654             [self release];
1655             return nil;
1656         }
1657     }
1659     fInfo = tr_torrentInfo(fHandle);
1660     
1661     tr_torrentSetQueueStartCallback(fHandle, startQueueCallback, self);
1662     tr_torrentSetCompletenessCallback(fHandle, completenessChangeCallback, self);
1663     tr_torrentSetRatioLimitHitCallback(fHandle, ratioLimitHitCallback, self);
1664     tr_torrentSetIdleLimitHitCallback(fHandle, idleLimitHitCallback, self);
1665     tr_torrentSetMetadataCallback(fHandle, metadataCallback, self);
1666     
1667     fHashString = [[NSString alloc] initWithUTF8String: fInfo->hashString];
1668     
1669     fResumeOnWake = NO;
1670     
1671     //don't do after this point - it messes with auto-group functionality
1672     if (![self isMagnet])
1673         [self createFileList];
1674         
1675     fGroupValue = groupValue ? [groupValue intValue] : [[GroupsController groups] groupIndexForTorrent: self]; 
1676     
1677     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(checkGroupValueForRemoval:)
1678         name: @"GroupValueRemoved" object: nil];
1679     
1680     fTimeMachineExclude = [timeMachineExclude retain];
1681     [self update];
1682     
1683     return self;
1686 - (void) createFileList
1688     NSAssert(![self isMagnet], @"Cannot create a file list until the torrent is demagnetized");
1689     
1690     if ([self isFolder])
1691     {
1692         const NSInteger count = [self fileCount];
1693         NSMutableArray * fileList = [NSMutableArray arrayWithCapacity: count],
1694                     * flatFileList = [NSMutableArray arrayWithCapacity: count];
1695         
1696         for (NSInteger i = 0; i < count; i++)
1697         {
1698             tr_file * file = &fInfo->files[i];
1699             
1700             NSString * fullPath = [NSString stringWithUTF8String: file->name];
1701             NSArray * pathComponents = [fullPath pathComponents];
1702             NSAssert1([pathComponents count] >= 2, @"Not enough components in path %@", fullPath);
1703             
1704             NSString * path = [pathComponents objectAtIndex: 0];
1705             NSString * name = [pathComponents objectAtIndex: 1];
1706             
1707             if ([pathComponents count] > 2)
1708             {
1709                 //determine if folder node already exists
1710                 __block FileListNode * node = nil;
1711                 [fileList enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * searchNode, NSUInteger idx, BOOL * stop) {
1712                     if ([[searchNode name] isEqualToString: name] && [searchNode isFolder])
1713                     {
1714                         node = searchNode;
1715                         *stop = YES;
1716                     }
1717                 }];
1718                 
1719                 if (!node)
1720                 {
1721                     node = [[FileListNode alloc] initWithFolderName: name path: path torrent: self];
1722                     [fileList addObject: node];
1723                     [node release];
1724                 }
1725                 
1726                 NSMutableArray * trimmedComponents = [NSMutableArray arrayWithArray: [pathComponents subarrayWithRange: NSMakeRange(2, [pathComponents count]-2)]];
1727                 
1728                 [node insertIndex: i withSize: file->length];
1729                 [self insertPath: trimmedComponents forParent: node fileSize: file->length index: i flatList: flatFileList];
1730             }
1731             else
1732             {
1733                 FileListNode * node = [[FileListNode alloc] initWithFileName: name path: path size: file->length index: i torrent: self];
1734                 [fileList addObject: node];
1735                 [flatFileList addObject: node];
1736                 [node release];
1737             }
1738         }
1739         
1740         [self sortFileList: fileList];
1741         [self sortFileList: flatFileList];
1742         
1743         fFileList = [[NSArray alloc] initWithArray: fileList];
1744         fFlatFileList = [[NSArray alloc] initWithArray: flatFileList];
1745     }
1746     else
1747     {
1748         FileListNode * node = [[FileListNode alloc] initWithFileName: [self name] path: @"" size: [self size] index: 0 torrent: self];
1749         fFileList = [[NSArray arrayWithObject: node] retain];
1750         fFlatFileList = [fFileList retain];
1751         [node release];
1752     }
1755 - (void) insertPath: (NSMutableArray *) components forParent: (FileListNode *) parent fileSize: (uint64_t) size
1756     index: (NSInteger) index flatList: (NSMutableArray *) flatFileList
1758     NSString * name = [components objectAtIndex: 0];
1759     const BOOL isFolder = [components count] > 1;
1760     
1761     //determine if folder node already exists
1762     __block FileListNode * node = nil;
1763     if (isFolder)
1764     {
1765         [[parent children] enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * searchNode, NSUInteger idx, BOOL * stop) {
1766             if ([[searchNode name] isEqualToString: name] && [searchNode isFolder])
1767             {
1768                 node = searchNode;
1769                 *stop = YES;
1770             }
1771         }];
1772     }
1773     
1774     //create new folder or file if it doesn't already exist
1775     if (!node)
1776     {
1777         NSString * path = [[parent path] stringByAppendingPathComponent: [parent name]];
1778         if (isFolder)
1779             node = [[FileListNode alloc] initWithFolderName: name path: path torrent: self];
1780         else
1781         {
1782             node = [[FileListNode alloc] initWithFileName: name path: path size: size index: index torrent: self];
1783             [flatFileList addObject: node];
1784         }
1785         
1786         [parent insertChild: node];
1787         [node release];
1788     }
1789     
1790     if (isFolder)
1791     {
1792         [node insertIndex: index withSize: size];
1793         
1794         [components removeObjectAtIndex: 0];
1795         [self insertPath: components forParent: node fileSize: size index: index flatList: flatFileList];
1796     }
1799 - (void) sortFileList: (NSMutableArray *) fileNodes
1801     NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey: @"name" ascending: YES selector: @selector(localizedStandardCompare:)];
1802     [fileNodes sortUsingDescriptors: [NSArray arrayWithObject: descriptor]];
1803     
1804     [fileNodes enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * node, NSUInteger idx, BOOL * stop) {
1805         if ([node isFolder])
1806             [self sortFileList: [node children]];
1807     }];
1810 - (void) startQueue
1812     [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
1815 //status has been retained
1816 - (void) completenessChange: (NSDictionary *) statusInfo
1818     fStat = tr_torrentStat(fHandle); //don't call update yet to avoid auto-stop
1819     
1820     switch ([[statusInfo objectForKey: @"Status"] intValue])
1821     {
1822         case TR_SEED:
1823         case TR_PARTIAL_SEED:
1824             //simpler to create a new dictionary than to use statusInfo - avoids retention chicanery
1825             [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedDownloading" object: self
1826                 userInfo: [NSDictionary dictionaryWithObject: [statusInfo objectForKey: @"WasRunning"] forKey: @"WasRunning"]];
1827             
1828             //quarantine the finished data
1829             NSString * dataLocation = [[self currentDirectory] stringByAppendingPathComponent: [self name]];
1830             FSRef ref;
1831             if (FSPathMakeRef((const UInt8 *)[dataLocation UTF8String], &ref, NULL) == noErr)
1832             {
1833                 NSDictionary * quarantineProperties = [NSDictionary dictionaryWithObject: (NSString *)kLSQuarantineTypeOtherDownload forKey: (NSString *)kLSQuarantineTypeKey];
1834                 if (LSSetItemAttribute(&ref, kLSRolesAll, kLSItemQuarantineProperties, quarantineProperties) != noErr)
1835                     NSLog(@"Failed to quarantine: %@", dataLocation);
1836             }
1837             else
1838                 NSLog(@"Could not find file to quarantine: %@", dataLocation);
1839             
1840             break;
1841         
1842         case TR_LEECH:
1843             [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentRestartedDownloading" object: self];
1844             break;
1845     }
1846     [statusInfo release];
1847     
1848     [self update];
1849     [self updateTimeMachineExclude];
1852 - (void) ratioLimitHit
1854     fStat = tr_torrentStat(fHandle);
1855     
1856     [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedSeeding" object: self];
1859 - (void) idleLimitHit
1861     fStat = tr_torrentStat(fHandle);
1862     
1863     [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedSeeding" object: self];
1866 - (void) metadataRetrieved
1868     fStat = tr_torrentStat(fHandle);
1869     
1870     [self createFileList];
1871     
1872     [[NSNotificationCenter defaultCenter] postNotificationName: @"ResetInspector" object: self];
1875 - (BOOL) shouldShowEta
1877     if (fStat->activity == TR_STATUS_DOWNLOAD)
1878         return YES;
1879     else if ([self isSeeding])
1880     {
1881         //ratio: show if it's set at all
1882         if (tr_torrentGetSeedRatio(fHandle, NULL))
1883             return YES;
1884         
1885         //idle: show only if remaining time is less than cap
1886         if (fStat->etaIdle != TR_ETA_NOT_AVAIL && fStat->etaIdle < ETA_IDLE_DISPLAY_SEC)
1887             return YES;
1888     }
1889     
1890     return NO;
1893 - (NSString *) etaString
1895     NSInteger eta;
1896     BOOL fromIdle;
1897     //don't check for both, since if there's a regular ETA, the torrent isn't idle so it's meaningless
1898     if (fStat->eta != TR_ETA_NOT_AVAIL && fStat->eta != TR_ETA_UNKNOWN)
1899     {
1900         eta = fStat->eta;
1901         fromIdle = NO;
1902     }
1903     else if (fStat->etaIdle != TR_ETA_NOT_AVAIL && fStat->etaIdle < ETA_IDLE_DISPLAY_SEC)
1904     {
1905         eta = fStat->etaIdle;
1906         fromIdle = YES;
1907     }
1908     else
1909         return NSLocalizedString(@"remaining time unknown", "Torrent -> eta string");
1910     
1911     NSString * idleString = [NSString stringWithFormat: NSLocalizedString(@"%@ remaining", "Torrent -> eta string"),
1912                                 [NSString timeString: eta showSeconds: YES maxFields: 2]];
1913     if (fromIdle)
1914         idleString = [idleString stringByAppendingFormat: @" (%@)", NSLocalizedString(@"inactive", "Torrent -> eta string")];
1915     
1916     return idleString;
1919 - (void) setTimeMachineExclude: (BOOL) exclude forPath: (NSString *) path
1921     CSBackupSetItemExcluded((CFURLRef)[NSURL fileURLWithPath: path], exclude, true);
1924 @end