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