source: trunk/macosx/Torrent.m @ 13379

Last change on this file since 13379 was 13379, checked in by livings124, 10 years ago

Explicitly create an ivar for Torrent's remove flag

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