source: trunk/macosx/Torrent.m @ 12227

Last change on this file since 12227 was 12227, checked in by livings124, 11 years ago

#4055 Fix a memory leak when removing a transfer + data file

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