source: trunk/macosx/Torrent.m @ 13187

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

remove redundant code in Torrent's canChangeDownloadCheckForFile:, and enumerate of the files in canChangeDownloadCheckForFiles: concurrently.

  • Property svn:keywords set to Date Rev Author Id
File size: 60.9 KB
Line 
1/******************************************************************************
2 * $Id: Torrent.m 13187 2012-01-31 04:22:51Z 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        timeMachineExcludeLocation: (NSString *) timeMachineExclude
42        downloadFolder: (NSString *) downloadFolder
43        legacyIncompleteFolder: (NSString *) incompleteFolder;
44
45- (void) createFileList;
46- (void) insertPath: (NSMutableArray *) components forParent: (FileListNode *) parent fileSize: (uint64_t) size
47    index: (NSInteger) index flatList: (NSMutableArray *) flatFileList;
48- (void) sortFileList: (NSMutableArray *) fileNodes;
49
50- (void) startQueue;
51- (void) completenessChange: (NSDictionary *) statusInfo;
52- (void) ratioLimitHit;
53- (void) idleLimitHit;
54- (void) metadataRetrieved;
55
56- (BOOL) shouldShowEta;
57- (NSString *) etaString;
58
59- (void) setTimeMachineExclude: (BOOL) exclude forPath: (NSString *) path;
60
61@end
62
63void startQueueCallback(tr_torrent * torrent, void * torrentData)
64{
65    [(Torrent *)torrentData performSelectorOnMainThread: @selector(startQueue) withObject: nil waitUntilDone: NO];
66}
67
68void completenessChangeCallback(tr_torrent * torrent, tr_completeness status, bool wasRunning, void * torrentData)
69{
70    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
71   
72    NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: status], @"Status",
73                            [NSNumber numberWithBool: wasRunning], @"WasRunning", nil];
74    [(Torrent *)torrentData performSelectorOnMainThread: @selector(completenessChange:) withObject: dict waitUntilDone: NO];
75   
76    [pool drain];
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    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
97   
98    if (filename != NULL)
99        [Torrent trashFile: [NSString stringWithUTF8String: filename]];
100   
101    [pool drain];
102    return 0;
103}
104
105@implementation Torrent
106
107- (id) initWithPath: (NSString *) path location: (NSString *) location deleteTorrentFile: (BOOL) torrentDelete
108        lib: (tr_session *) lib
109{
110    self = [self initWithPath: path hash: nil torrentStruct: NULL magnetAddress: nil lib: lib
111            groupValue: nil
112            timeMachineExcludeLocation: nil
113            downloadFolder: location
114            legacyIncompleteFolder: nil];
115   
116    if (self)
117    {
118        if (torrentDelete && ![[self torrentLocation] isEqualToString: path])
119            [Torrent trashFile: path];
120    }
121    return self;
122}
123
124- (id) initWithTorrentStruct: (tr_torrent *) torrentStruct location: (NSString *) location lib: (tr_session *) lib
125{
126    self = [self initWithPath: nil hash: nil torrentStruct: torrentStruct magnetAddress: nil lib: lib
127            groupValue: nil
128            timeMachineExcludeLocation: nil
129            downloadFolder: location
130            legacyIncompleteFolder: nil];
131   
132    return self;
133}
134
135- (id) initWithMagnetAddress: (NSString *) address location: (NSString *) location lib: (tr_session *) lib
136{
137    self = [self initWithPath: nil hash: nil torrentStruct: nil magnetAddress: address
138            lib: lib groupValue: nil
139            timeMachineExcludeLocation: nil
140            downloadFolder: location legacyIncompleteFolder: nil];
141   
142    return self;
143}
144
145- (id) initWithHistory: (NSDictionary *) history lib: (tr_session *) lib forcePause: (BOOL) pause
146{
147    self = [self initWithPath: [history objectForKey: @"InternalTorrentPath"]
148                hash: [history objectForKey: @"TorrentHash"]
149                torrentStruct: NULL
150                magnetAddress: nil
151                lib: lib
152                groupValue: [history objectForKey: @"GroupValue"]
153                timeMachineExcludeLocation: [history objectForKey: @"TimeMachineExcludeLocation"]
154                downloadFolder: [history objectForKey: @"DownloadFolder"] //upgrading from versions < 1.80
155                legacyIncompleteFolder: [[history objectForKey: @"UseIncompleteFolder"] boolValue] //upgrading from versions < 1.80
156                                        ? [history objectForKey: @"IncompleteFolder"] : nil];
157   
158    if (self)
159    {
160        //start transfer
161        NSNumber * active;
162        if (!pause && (active = [history objectForKey: @"Active"]) && [active boolValue])
163        {
164            fStat = tr_torrentStat(fHandle);
165            [self startTransferNoQueue];
166        }
167       
168        //upgrading from versions < 1.30: get old added, activity, and done dates
169        NSDate * date;
170        if ((date = [history objectForKey: @"Date"]))
171            tr_torrentSetAddedDate(fHandle, [date timeIntervalSince1970]);
172        if ((date = [history objectForKey: @"DateActivity"]))
173            tr_torrentSetActivityDate(fHandle, [date timeIntervalSince1970]);
174        if ((date = [history objectForKey: @"DateCompleted"]))
175            tr_torrentSetDoneDate(fHandle, [date timeIntervalSince1970]);
176       
177        //upgrading from versions < 1.60: get old stop ratio settings
178        NSNumber * ratioSetting;
179        if ((ratioSetting = [history objectForKey: @"RatioSetting"]))
180        {
181            switch ([ratioSetting intValue])
182            {
183                case NSOnState: [self setRatioSetting: TR_RATIOLIMIT_SINGLE]; break;
184                case NSOffState: [self setRatioSetting: TR_RATIOLIMIT_UNLIMITED]; break;
185                case NSMixedState: [self setRatioSetting: TR_RATIOLIMIT_GLOBAL]; break;
186            }
187        }
188        NSNumber * ratioLimit;
189        if ((ratioLimit = [history objectForKey: @"RatioLimit"]))
190            [self setRatioLimit: [ratioLimit floatValue]];
191    }
192    return self;
193}
194
195- (NSDictionary *) history
196{
197    NSMutableDictionary * history = [NSMutableDictionary dictionaryWithObjectsAndKeys:
198                                        [self torrentLocation], @"InternalTorrentPath",
199                                        [self hashString], @"TorrentHash",
200                                        [NSNumber numberWithBool: [self isActive]], @"Active",
201                                        [NSNumber numberWithBool: [self waitingToStart]], @"WaitToStart",
202                                        [NSNumber numberWithInt: fGroupValue], @"GroupValue", nil];
203   
204    if (fTimeMachineExclude)
205        [history setObject: fTimeMachineExclude forKey: @"TimeMachineExcludeLocation"];
206   
207    return history;
208}
209
210- (void) dealloc
211{
212    [[NSNotificationCenter defaultCenter] removeObserver: self];
213   
214    if (fFileStat)
215        tr_torrentFilesFree(fFileStat, [self fileCount]);
216   
217    [fPreviousFinishedIndexes release];
218    [fPreviousFinishedIndexesDate release];
219   
220    [fHashString release];
221   
222    [fIcon release];
223   
224    [fFileList release];
225    [fFlatFileList release];
226   
227    [fTimeMachineExclude release];
228   
229    [super dealloc];
230}
231
232- (NSString *) description
233{
234    return [@"Torrent: " stringByAppendingString: [self name]];
235}
236
237- (id) copyWithZone: (NSZone *) zone
238{
239    return [self retain];
240}
241
242- (void) closeRemoveTorrent: (BOOL) trashFiles
243{
244    //allow the file to be indexed by Time Machine
245    if (fTimeMachineExclude)
246    {
247        [self setTimeMachineExclude: NO forPath: fTimeMachineExclude];
248        [fTimeMachineExclude release];
249        fTimeMachineExclude = nil;
250    }
251   
252    tr_torrentRemove(fHandle, trashFiles, trashDataFile);
253}
254
255- (void) changeDownloadFolderBeforeUsing: (NSString *) folder
256{
257     tr_torrentSetDownloadDir(fHandle, [folder UTF8String]);
258     [self updateTimeMachineExclude];
259}
260
261- (NSString *) currentDirectory
262{
263    return [NSString stringWithUTF8String: tr_torrentGetCurrentDir(fHandle)];
264}
265
266- (void) getAvailability: (int8_t *) tab size: (NSInteger) size
267{
268    tr_torrentAvailability(fHandle, tab, size);
269}
270
271- (void) getAmountFinished: (float *) tab size: (NSInteger) size
272{
273    tr_torrentAmountFinished(fHandle, tab, size);
274}
275
276- (NSIndexSet *) previousFinishedPieces
277{
278    //if the torrent hasn't been seen in a bit, and therefore hasn't been refreshed, return nil
279    if (fPreviousFinishedIndexesDate && [fPreviousFinishedIndexesDate timeIntervalSinceNow] > -2.0)
280        return fPreviousFinishedIndexes;
281    else
282        return nil;
283}
284
285- (void) setPreviousFinishedPieces: (NSIndexSet *) indexes
286{
287    [fPreviousFinishedIndexes release];
288    fPreviousFinishedIndexes = [indexes retain];
289   
290    [fPreviousFinishedIndexesDate release];
291    fPreviousFinishedIndexesDate = indexes != nil ? [[NSDate alloc] init] : nil;
292}
293
294- (void) update
295{
296    //get previous stalled value before update
297    const BOOL wasStalled = fStat != NULL && [self isStalled];
298   
299    fStat = tr_torrentStat(fHandle);
300   
301    //make sure the "active" filter is updated when stalled-ness changes
302    if (wasStalled != [self isStalled])
303        [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
304   
305    //when the data first appears, update time machine exclusion
306    if (!fTimeMachineExclude)
307        [self updateTimeMachineExclude];
308}
309
310- (void) startTransferIgnoringQueue: (BOOL) ignoreQueue
311{
312    if ([self alertForRemainingDiskSpace])
313    {
314        ignoreQueue ? tr_torrentStartNow(fHandle) : tr_torrentStart(fHandle);
315        [self update];
316       
317        //capture, specifically, stop-seeding settings changing to unlimited
318        [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil];
319    }
320}
321
322- (void) startTransferNoQueue
323{
324    [self startTransferIgnoringQueue: YES];
325}
326
327- (void) startTransfer
328{
329    [self startTransferIgnoringQueue: NO];
330}
331
332- (void) stopTransfer
333{
334    tr_torrentStop(fHandle);
335    [self update];
336}
337
338- (void) sleep
339{
340    if ((fResumeOnWake = [self isActive]))
341        tr_torrentStop(fHandle);
342}
343
344- (void) wakeUp
345{
346    if (fResumeOnWake)
347    {
348        tr_ninf( fInfo->name, "restarting because of wakeUp" );
349        tr_torrentStart(fHandle);
350    }
351}
352
353- (NSInteger) queuePosition
354{
355    return fStat->queuePosition;
356}
357
358- (void) setQueuePosition: (NSUInteger) index
359{
360    tr_torrentSetQueuePosition(fHandle, index);
361}
362
363- (void) manualAnnounce
364{
365    tr_torrentManualUpdate(fHandle);
366}
367
368- (BOOL) canManualAnnounce
369{
370    return tr_torrentCanManualUpdate(fHandle);
371}
372
373- (void) resetCache
374{
375    tr_torrentVerify(fHandle);
376    [self update];
377}
378
379- (BOOL) isMagnet
380{
381    return !tr_torrentHasMetadata(fHandle);
382}
383
384- (NSString *) magnetLink
385{
386    return [NSString stringWithUTF8String: tr_torrentGetMagnetLink(fHandle)];
387}
388
389- (CGFloat) ratio
390{
391    return fStat->ratio;
392}
393
394- (tr_ratiolimit) ratioSetting
395{
396    return tr_torrentGetRatioMode(fHandle);
397}
398
399- (void) setRatioSetting: (tr_ratiolimit) setting
400{
401    tr_torrentSetRatioMode(fHandle, setting);
402}
403
404- (CGFloat) ratioLimit
405{
406    return tr_torrentGetRatioLimit(fHandle);
407}
408
409- (void) setRatioLimit: (CGFloat) limit
410{
411    NSAssert(limit >= 0, @"Ratio cannot be negative");
412    tr_torrentSetRatioLimit(fHandle, limit);
413}
414
415- (CGFloat) progressStopRatio
416{
417    return fStat->seedRatioPercentDone;
418}
419
420- (tr_idlelimit) idleSetting
421{
422    return tr_torrentGetIdleMode(fHandle);
423}
424
425- (void) setIdleSetting: (tr_idlelimit) setting
426{
427    tr_torrentSetIdleMode(fHandle, setting);
428}
429
430- (NSUInteger) idleLimitMinutes
431{
432    return tr_torrentGetIdleLimit(fHandle);
433}
434
435- (void) setIdleLimitMinutes: (NSUInteger) limit
436{
437    NSAssert(limit > 0, @"Idle limit must be greater than zero");
438    tr_torrentSetIdleLimit(fHandle, limit);
439}
440
441- (BOOL) usesSpeedLimit: (BOOL) upload
442{
443    return tr_torrentUsesSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN);
444}
445
446- (void) setUseSpeedLimit: (BOOL) use upload: (BOOL) upload
447{
448    tr_torrentUseSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN, use);
449}
450
451- (NSInteger) speedLimit: (BOOL) upload
452{
453    return tr_torrentGetSpeedLimit_KBps(fHandle, upload ? TR_UP : TR_DOWN);
454}
455
456- (void) setSpeedLimit: (NSInteger) limit upload: (BOOL) upload
457{
458    tr_torrentSetSpeedLimit_KBps(fHandle, upload ? TR_UP : TR_DOWN, limit);
459}
460
461- (BOOL) usesGlobalSpeedLimit
462{
463    return tr_torrentUsesSessionLimits(fHandle);
464}
465
466- (void) setUseGlobalSpeedLimit: (BOOL) use
467{
468    tr_torrentUseSessionLimits(fHandle, use);
469}
470
471- (void) setMaxPeerConnect: (uint16_t) count
472{
473    NSAssert(count > 0, @"max peer count must be greater than 0");
474   
475    tr_torrentSetPeerLimit(fHandle, count);
476}
477
478- (uint16_t) maxPeerConnect
479{
480    return tr_torrentGetPeerLimit(fHandle);
481}
482- (BOOL) waitingToStart
483{
484    return fStat->activity == TR_STATUS_DOWNLOAD_WAIT || fStat->activity == TR_STATUS_SEED_WAIT;
485}
486
487- (tr_priority_t) priority
488{
489    return tr_torrentGetPriority(fHandle);
490}
491
492- (void) setPriority: (tr_priority_t) priority
493{
494    return tr_torrentSetPriority(fHandle, priority);
495}
496
497#warning when 10.6-only use recycleURLs:completionHandler:
498+ (void) trashFile: (NSString *) path
499{
500    //attempt to move to trash
501    if (![[NSWorkspace sharedWorkspace] performFileOperation: NSWorkspaceRecycleOperation
502        source: [path stringByDeletingLastPathComponent] destination: @""
503        files: [NSArray arrayWithObject: [path lastPathComponent]] tag: nil])
504    {
505        //if cannot trash, just delete it (will work if it's on a remote volume)
506        NSError * error;
507        if (![[NSFileManager defaultManager] removeItemAtPath: path error: &error])
508            NSLog(@"Could not trash %@: %@", path, [error localizedDescription]);
509    }
510}
511
512- (void) moveTorrentDataFileTo: (NSString *) folder
513{
514    NSString * oldFolder = [self currentDirectory];
515    if ([oldFolder isEqualToString: folder])
516        return;
517   
518    //check if moving inside itself
519    NSArray * oldComponents = [oldFolder pathComponents],
520            * newComponents = [folder pathComponents];
521    const NSInteger oldCount = [oldComponents count];
522   
523    if (oldCount < [newComponents count] && [[newComponents objectAtIndex: oldCount] isEqualToString: [self name]]
524        && [folder hasPrefix: oldFolder])
525    {
526        NSAlert * alert = [[NSAlert alloc] init];
527        [alert setMessageText: NSLocalizedString(@"A folder cannot be moved to inside itself.",
528                                                    "Move inside itself alert -> title")];
529        [alert setInformativeText: [NSString stringWithFormat:
530                        NSLocalizedString(@"The move operation of \"%@\" cannot be done.",
531                                            "Move inside itself alert -> message"), [self name]]];
532        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move inside itself alert -> button")];
533       
534        [alert runModal];
535        [alert release];
536       
537        return;
538    }
539   
540    volatile int status;
541    tr_torrentSetLocation(fHandle, [folder UTF8String], YES, NULL, &status);
542   
543    while (status == TR_LOC_MOVING) //block while moving (for now)
544        [NSThread sleepForTimeInterval: 0.05];
545   
546    if (status == TR_LOC_DONE)
547        [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateStats" object: nil];
548    else
549    {
550        NSAlert * alert = [[NSAlert alloc] init];
551        [alert setMessageText: NSLocalizedString(@"There was an error moving the data file.", "Move error alert -> title")];
552        [alert setInformativeText: [NSString stringWithFormat:
553                NSLocalizedString(@"The move operation of \"%@\" cannot be done.", "Move error alert -> message"), [self name]]];
554        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move error alert -> button")];
555       
556        [alert runModal];
557        [alert release];
558    }
559   
560    [self updateTimeMachineExclude];
561}
562
563- (void) copyTorrentFileTo: (NSString *) path
564{
565    [[NSFileManager defaultManager] copyItemAtPath: [self torrentLocation] toPath: path error: NULL];
566}
567
568- (BOOL) alertForRemainingDiskSpace
569{
570    if ([self allDownloaded] || ![fDefaults boolForKey: @"WarningRemainingSpace"])
571        return YES;
572   
573    NSString * downloadFolder = [self currentDirectory];
574    NSDictionary * systemAttributes;
575    if ((systemAttributes = [[NSFileManager defaultManager] attributesOfFileSystemForPath: downloadFolder error: NULL]))
576    {
577        const uint64_t remainingSpace = [[systemAttributes objectForKey: NSFileSystemFreeSize] unsignedLongLongValue];
578       
579        //if the remaining space is greater than the size left, then there is enough space regardless of preallocation
580        if (remainingSpace < [self sizeLeft] && remainingSpace < tr_torrentGetBytesLeftToAllocate(fHandle))
581        {
582            NSString * volumeName = [[[NSFileManager defaultManager] componentsToDisplayForPath: downloadFolder] objectAtIndex: 0];
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 kGenericFolderIcon stuff with NSImageNameFolder on 10.6
615    if (!fIcon)
616        fIcon = [[[NSWorkspace sharedWorkspace] iconForFileType: [self isFolder] ? NSFileTypeForHFSTypeCode(kGenericFolderIcon)
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) removeTrackers: (NSSet *) trackers
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 (![trackers containsObject: [NSString stringWithUTF8String: fInfo->trackers[i].announce]])
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 && fStat->activity != TR_STATUS_DOWNLOAD_WAIT && fStat->activity != TR_STATUS_SEED_WAIT;
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 ([self isFinishedSeeding])
1052                    string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1053                else
1054                    string = NSLocalizedString(@"Paused", "Torrent -> status string");
1055                break;
1056           
1057            case TR_STATUS_DOWNLOAD_WAIT:
1058                string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis];
1059                break;
1060               
1061            case TR_STATUS_SEED_WAIT:
1062                string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
1063                break;
1064           
1065            case TR_STATUS_CHECK_WAIT:
1066                string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1067                break;
1068
1069            case TR_STATUS_CHECK:
1070                string = [NSString stringWithFormat: @"%@ (%@)",
1071                            NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
1072                            [NSString percentString: [self checkingProgress] longDecimals: YES]];
1073                break;
1074
1075            case TR_STATUS_DOWNLOAD:
1076                if ([self totalPeersConnected] != 1)
1077                    string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of %d peers",
1078                                                    "Torrent -> status string"), [self peersSendingToUs], [self totalPeersConnected]];
1079                else
1080                    string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of 1 peer",
1081                                                    "Torrent -> status string"), [self peersSendingToUs]];
1082               
1083                const NSInteger webSeedCount = fStat->webseedsSendingToUs;
1084                if (webSeedCount > 0)
1085                {
1086                    NSString * webSeedString;
1087                    if (webSeedCount == 1)
1088                        webSeedString = NSLocalizedString(@"web seed", "Torrent -> status string");
1089                    else
1090                        webSeedString = [NSString stringWithFormat: NSLocalizedString(@"%d web seeds", "Torrent -> status string"),
1091                                                                    webSeedCount];
1092                   
1093                    string = [string stringByAppendingFormat: @" + %@", webSeedString];
1094                }
1095               
1096                break;
1097
1098            case TR_STATUS_SEED:
1099                if ([self totalPeersConnected] != 1)
1100                    string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of %d peers", "Torrent -> status string"),
1101                                                    [self peersGettingFromUs], [self totalPeersConnected]];
1102                else
1103                    string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of 1 peer", "Torrent -> status string"),
1104                                                    [self peersGettingFromUs]];
1105        }
1106       
1107        if ([self isStalled])
1108            string = [NSLocalizedString(@"Stalled", "Torrent -> status string") stringByAppendingFormat: @", %@", string];
1109    }
1110   
1111    //append even if error
1112    if ([self isActive] && ![self isChecking])
1113    {
1114        if (fStat->activity == TR_STATUS_DOWNLOAD)
1115            string = [string stringByAppendingFormat: @" - %@: %@, %@: %@",
1116                        NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]],
1117                        NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1118        else
1119            string = [string stringByAppendingFormat: @" - %@: %@",
1120                        NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1121    }
1122   
1123    return string;
1124}
1125
1126- (NSString *) shortStatusString
1127{
1128    NSString * string;
1129   
1130    switch (fStat->activity)
1131    {
1132        case TR_STATUS_STOPPED:
1133            if ([self isFinishedSeeding])
1134                string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1135            else
1136                string = NSLocalizedString(@"Paused", "Torrent -> status string");
1137            break;
1138       
1139        case TR_STATUS_DOWNLOAD_WAIT:
1140            string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis];
1141            break;
1142           
1143        case TR_STATUS_SEED_WAIT:
1144            string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
1145            break;
1146
1147        case TR_STATUS_CHECK_WAIT:
1148            string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1149            break;
1150
1151        case TR_STATUS_CHECK:
1152            string = [NSString stringWithFormat: @"%@ (%@)",
1153                        NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
1154                        [NSString percentString: [self checkingProgress] longDecimals: YES]];
1155            break;
1156       
1157        case TR_STATUS_DOWNLOAD:
1158            string = [NSString stringWithFormat: @"%@: %@, %@: %@",
1159                            NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]],
1160                            NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1161            break;
1162       
1163        case TR_STATUS_SEED:
1164            string = [NSString stringWithFormat: @"%@: %@, %@: %@",
1165                            NSLocalizedString(@"Ratio", "Torrent -> status string"), [NSString stringForRatio: [self ratio]],
1166                            NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1167    }
1168   
1169    return string;
1170}
1171
1172- (NSString *) remainingTimeString
1173{
1174    if ([self shouldShowEta])
1175        return [self etaString];
1176    else
1177        return [self shortStatusString];
1178}
1179
1180- (NSString *) stateString
1181{
1182    switch (fStat->activity)
1183    {
1184        case TR_STATUS_STOPPED:
1185        case TR_STATUS_DOWNLOAD_WAIT:
1186        case TR_STATUS_SEED_WAIT:
1187        {
1188            NSString * string = NSLocalizedString(@"Paused", "Torrent -> status string");
1189           
1190            NSString * extra = nil;
1191            if ([self waitingToStart])
1192            {
1193                extra = fStat->activity == TR_STATUS_DOWNLOAD_WAIT
1194                        ? NSLocalizedString(@"Waiting to download", "Torrent -> status string")
1195                        : NSLocalizedString(@"Waiting to seed", "Torrent -> status string");
1196            }
1197            else if ([self isFinishedSeeding])
1198                extra = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1199            else;
1200       
1201            return extra ? [string stringByAppendingFormat: @" (%@)", extra] : string;
1202        }
1203       
1204        case TR_STATUS_CHECK_WAIT:
1205            return [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1206
1207        case TR_STATUS_CHECK:
1208            return [NSString stringWithFormat: @"%@ (%@)",
1209                    NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
1210                    [NSString percentString: [self checkingProgress] longDecimals: YES]];
1211
1212        case TR_STATUS_DOWNLOAD:
1213            return NSLocalizedString(@"Downloading", "Torrent -> status string");
1214
1215        case TR_STATUS_SEED:
1216            return NSLocalizedString(@"Seeding", "Torrent -> status string");
1217    }
1218}
1219
1220- (NSInteger) totalPeersConnected
1221{
1222    return fStat->peersConnected;
1223}
1224
1225- (NSInteger) totalPeersTracker
1226{
1227    return fStat->peersFrom[TR_PEER_FROM_TRACKER];
1228}
1229
1230- (NSInteger) totalPeersIncoming
1231{
1232    return fStat->peersFrom[TR_PEER_FROM_INCOMING];
1233}
1234
1235- (NSInteger) totalPeersCache
1236{
1237    return fStat->peersFrom[TR_PEER_FROM_RESUME];
1238}
1239
1240- (NSInteger) totalPeersPex
1241{
1242    return fStat->peersFrom[TR_PEER_FROM_PEX];
1243}
1244
1245- (NSInteger) totalPeersDHT
1246{
1247    return fStat->peersFrom[TR_PEER_FROM_DHT];
1248}
1249
1250- (NSInteger) totalPeersLocal
1251{
1252    return fStat->peersFrom[TR_PEER_FROM_LPD];
1253}
1254
1255- (NSInteger) totalPeersLTEP
1256{
1257    return fStat->peersFrom[TR_PEER_FROM_LTEP];
1258}
1259
1260- (NSInteger) peersSendingToUs
1261{
1262    return fStat->peersSendingToUs;
1263}
1264
1265- (NSInteger) peersGettingFromUs
1266{
1267    return fStat->peersGettingFromUs;
1268}
1269
1270- (CGFloat) downloadRate
1271{
1272    return fStat->pieceDownloadSpeed_KBps;
1273}
1274
1275- (CGFloat) uploadRate
1276{
1277    return fStat->pieceUploadSpeed_KBps;
1278}
1279
1280- (CGFloat) totalRate
1281{
1282    return [self downloadRate] + [self uploadRate];
1283}
1284
1285- (uint64_t) haveVerified
1286{
1287    return fStat->haveValid;
1288}
1289
1290- (uint64_t) haveTotal
1291{
1292    return [self haveVerified] + fStat->haveUnchecked;
1293}
1294
1295- (uint64_t) totalSizeSelected
1296{
1297    return fStat->sizeWhenDone;
1298}
1299
1300- (uint64_t) downloadedTotal
1301{
1302    return fStat->downloadedEver;
1303}
1304
1305- (uint64_t) uploadedTotal
1306{
1307    return fStat->uploadedEver;
1308}
1309
1310- (uint64_t) failedHash
1311{
1312    return fStat->corruptEver;
1313}
1314
1315- (NSInteger) groupValue
1316{
1317    return fGroupValue;
1318}
1319
1320- (void) setGroupValue: (NSInteger) goupValue
1321{
1322    fGroupValue = goupValue;
1323}
1324
1325- (NSInteger) groupOrderValue
1326{
1327    return [[GroupsController groups] rowValueForIndex: fGroupValue];
1328}
1329
1330- (void) checkGroupValueForRemoval: (NSNotification *) notification
1331{
1332    if (fGroupValue != -1 && [[[notification userInfo] objectForKey: @"Index"] integerValue] == fGroupValue)
1333        fGroupValue = -1;
1334}
1335
1336- (NSArray *) fileList
1337{
1338    return fFileList;
1339}
1340
1341- (NSArray *) flatFileList
1342{
1343    return fFlatFileList;
1344}
1345
1346- (NSInteger) fileCount
1347{
1348    return fInfo->fileCount;
1349}
1350
1351- (void) updateFileStat
1352{
1353    if (fFileStat)
1354        tr_torrentFilesFree(fFileStat, [self fileCount]);
1355   
1356    fFileStat = tr_torrentFiles(fHandle, NULL);
1357}
1358
1359- (CGFloat) fileProgress: (FileListNode *) node
1360{
1361    if ([self fileCount] == 1 || [self isComplete])
1362        return [self progress];
1363   
1364    if (!fFileStat)
1365        [self updateFileStat];
1366   
1367    NSIndexSet * indexSet = [node indexes];
1368   
1369    if ([indexSet count] == 1)
1370        return fFileStat[[indexSet firstIndex]].progress;
1371   
1372    uint64_t have = 0;
1373    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1374        have += fFileStat[index].bytesCompleted;
1375   
1376    NSAssert([node size], @"directory in torrent file has size 0");
1377    return (CGFloat)have / [node size];
1378}
1379
1380- (BOOL) canChangeDownloadCheckForFile: (NSUInteger) index
1381{
1382    NSAssert2(index < [self fileCount], @"Index %d is greater than file count %d", index, [self fileCount]);
1383   
1384    return [self canChangeDownloadCheckForFiles: [NSIndexSet indexSetWithIndex: index]];
1385}
1386
1387- (BOOL) canChangeDownloadCheckForFiles: (NSIndexSet *) indexSet
1388{
1389    if ([self fileCount] == 1 || [self isComplete])
1390        return NO;
1391   
1392    if (!fFileStat)
1393        [self updateFileStat];
1394   
1395    __block BOOL canChange = NO;
1396    [indexSet enumerateIndexesWithOptions: NSEnumerationConcurrent usingBlock: ^(NSUInteger index, BOOL *stop) {
1397        if (fFileStat[index].progress < 1.0)
1398        {
1399            canChange = YES;
1400            *stop = YES;
1401        }
1402    }];
1403    return canChange;
1404}
1405
1406- (NSInteger) checkForFiles: (NSIndexSet *) indexSet
1407{
1408    BOOL onState = NO, offState = NO;
1409    for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1410    {
1411        if (!fInfo->files[index].dnd || ![self canChangeDownloadCheckForFile: index])
1412            onState = YES;
1413        else
1414            offState = YES;
1415       
1416        if (onState && offState)
1417            return NSMixedState;
1418    }
1419    return onState ? NSOnState : NSOffState;
1420}
1421
1422- (void) setFileCheckState: (NSInteger) state forIndexes: (NSIndexSet *) indexSet
1423{
1424    NSUInteger count = [indexSet count];
1425    tr_file_index_t * files = malloc(count * sizeof(tr_file_index_t));
1426    for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++)
1427        files[i] = index;
1428   
1429    tr_torrentSetFileDLs(fHandle, files, count, state != NSOffState);
1430    free(files);
1431   
1432    [self update];
1433    [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFileCheckChange" object: self];
1434}
1435
1436- (void) setFilePriority: (tr_priority_t) priority forIndexes: (NSIndexSet *) indexSet
1437{
1438    const NSUInteger count = [indexSet count];
1439    tr_file_index_t * files = tr_malloc(count * sizeof(tr_file_index_t));
1440    for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++)
1441        files[i] = index;
1442   
1443    tr_torrentSetFilePriorities(fHandle, files, count, priority);
1444    tr_free(files);
1445}
1446
1447- (BOOL) hasFilePriority: (tr_priority_t) priority forIndexes: (NSIndexSet *) indexSet
1448{
1449    for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1450        if (priority == fInfo->files[index].priority && [self canChangeDownloadCheckForFile: index])
1451            return YES;
1452    return NO;
1453}
1454
1455- (NSSet *) filePrioritiesForIndexes: (NSIndexSet *) indexSet
1456{
1457    BOOL low = NO, normal = NO, high = NO;
1458    NSMutableSet * priorities = [NSMutableSet setWithCapacity: MIN([indexSet count], 3)];
1459   
1460    for (NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1461    {
1462        if (![self canChangeDownloadCheckForFile: index])
1463            continue;
1464       
1465        const tr_priority_t priority = fInfo->files[index].priority;
1466        switch (priority)
1467        {
1468            case TR_PRI_LOW:
1469                if (low)
1470                    continue;
1471                low = YES;
1472                break;
1473            case TR_PRI_NORMAL:
1474                if (normal)
1475                    continue;
1476                normal = YES;
1477                break;
1478            case TR_PRI_HIGH:
1479                if (high)
1480                    continue;
1481                high = YES;
1482                break;
1483            default:
1484                NSAssert2(NO, @"Unknown priority %d for file index %d", priority, index);
1485        }
1486       
1487        [priorities addObject: [NSNumber numberWithInteger: priority]];
1488        if (low && normal && high)
1489            break;
1490    }
1491    return priorities;
1492}
1493
1494- (NSDate *) dateAdded
1495{
1496    const time_t date = fStat->addedDate;
1497    return [NSDate dateWithTimeIntervalSince1970: date];
1498}
1499
1500- (NSDate *) dateCompleted
1501{
1502    const time_t date = fStat->doneDate;
1503    return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
1504}
1505
1506- (NSDate *) dateActivity
1507{
1508    const time_t date = fStat->activityDate;
1509    return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
1510}
1511
1512- (NSDate *) dateActivityOrAdd
1513{
1514    NSDate * date = [self dateActivity];
1515    return date ? date : [self dateAdded];
1516}
1517
1518- (NSInteger) secondsDownloading
1519{
1520    return fStat->secondsDownloading;
1521}
1522
1523- (NSInteger) secondsSeeding
1524{
1525    return fStat->secondsSeeding;
1526}
1527
1528- (NSInteger) stalledMinutes
1529{
1530    if (fStat->idleSecs == -1)
1531        return -1;
1532   
1533    return fStat->idleSecs / 60;
1534}
1535
1536- (BOOL) isStalled
1537{
1538    return fStat->isStalled;
1539}
1540
1541- (void) updateTimeMachineExclude
1542{
1543    NSString * currentLocation = ![self allDownloaded] ? [self dataLocation] : nil;
1544   
1545    //return if the locations are the same
1546    if (fTimeMachineExclude && currentLocation && [fTimeMachineExclude isEqualToString: currentLocation])
1547        return;
1548   
1549    //remove old location...
1550    if (fTimeMachineExclude)
1551    {
1552        [self setTimeMachineExclude: NO forPath: fTimeMachineExclude];
1553        [fTimeMachineExclude release];
1554        fTimeMachineExclude = nil;
1555    }
1556   
1557    //...set new location
1558    if (currentLocation)
1559    {
1560        [self setTimeMachineExclude: YES forPath: currentLocation];
1561        fTimeMachineExclude = [currentLocation retain];
1562    }
1563}
1564
1565- (NSInteger) stateSortKey
1566{
1567    if (![self isActive]) //paused
1568    {
1569        if ([self waitingToStart])
1570            return 1;
1571        else
1572            return 0;
1573    }
1574    else if ([self isSeeding]) //seeding
1575        return 10;
1576    else //downloading
1577        return 20;
1578}
1579
1580- (NSString *) trackerSortKey
1581{
1582    int count;
1583    tr_tracker_stat * stats = tr_torrentTrackers(fHandle, &count);
1584   
1585    NSString * best = nil;
1586   
1587    for (int i=0; i < count; ++i)
1588    {
1589        NSString * tracker = [NSString stringWithUTF8String: stats[i].host];
1590        if (!best || [tracker localizedCaseInsensitiveCompare: best] == NSOrderedAscending)
1591            best = tracker;
1592    }
1593   
1594    tr_torrentTrackersFree(stats, count);
1595    return best;
1596}
1597
1598- (tr_torrent *) torrentStruct
1599{
1600    return fHandle;
1601}
1602
1603- (NSURL *) previewItemURL
1604{
1605    NSString * location = [self dataLocation];
1606    return location ? [NSURL fileURLWithPath: location] : nil;
1607}
1608
1609@end
1610
1611@implementation Torrent (Private)
1612
1613- (id) initWithPath: (NSString *) path hash: (NSString *) hashString torrentStruct: (tr_torrent *) torrentStruct
1614        magnetAddress: (NSString *) magnetAddress lib: (tr_session *) lib
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_torrentSetQueueStartCallback(fHandle, startQueueCallback, self);
1664    tr_torrentSetCompletenessCallback(fHandle, completenessChangeCallback, self);
1665    tr_torrentSetRatioLimitHitCallback(fHandle, ratioLimitHitCallback, self);
1666    tr_torrentSetIdleLimitHitCallback(fHandle, idleLimitHitCallback, self);
1667    tr_torrentSetMetadataCallback(fHandle, metadataCallback, self);
1668   
1669    fHashString = [[NSString alloc] initWithUTF8String: fInfo->hashString];
1670   
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                __block FileListNode * node = nil;
1713                [fileList enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * searchNode, NSUInteger idx, BOOL * stop) {
1714                    if ([[searchNode name] isEqualToString: name] && [searchNode isFolder])
1715                    {
1716                        node = searchNode;
1717                        *stop = YES;
1718                    }
1719                }];
1720               
1721                if (!node)
1722                {
1723                    node = [[FileListNode alloc] initWithFolderName: name path: path torrent: self];
1724                    [fileList addObject: node];
1725                    [node release];
1726                }
1727               
1728                NSMutableArray * trimmedComponents = [NSMutableArray arrayWithArray: [pathComponents subarrayWithRange: NSMakeRange(2, [pathComponents count]-2)]];
1729               
1730                [node insertIndex: i withSize: file->length];
1731                [self insertPath: trimmedComponents forParent: node fileSize: file->length index: i flatList: flatFileList];
1732            }
1733            else
1734            {
1735                FileListNode * node = [[FileListNode alloc] initWithFileName: name path: path size: file->length index: i torrent: self];
1736                [fileList addObject: node];
1737                [flatFileList addObject: node];
1738                [node release];
1739            }
1740        }
1741       
1742        [self sortFileList: fileList];
1743        [self sortFileList: flatFileList];
1744       
1745        fFileList = [[NSArray alloc] initWithArray: fileList];
1746        fFlatFileList = [[NSArray alloc] initWithArray: flatFileList];
1747    }
1748    else
1749    {
1750        FileListNode * node = [[FileListNode alloc] initWithFileName: [self name] path: @"" size: [self size] index: 0 torrent: self];
1751        fFileList = [[NSArray arrayWithObject: node] retain];
1752        fFlatFileList = [fFileList retain];
1753        [node release];
1754    }
1755}
1756
1757- (void) insertPath: (NSMutableArray *) components forParent: (FileListNode *) parent fileSize: (uint64_t) size
1758    index: (NSInteger) index flatList: (NSMutableArray *) flatFileList
1759{
1760    NSString * name = [components objectAtIndex: 0];
1761    const BOOL isFolder = [components count] > 1;
1762   
1763    //determine if folder node already exists
1764    __block FileListNode * node = nil;
1765    if (isFolder)
1766    {
1767        [[parent children] enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * searchNode, NSUInteger idx, BOOL * stop) {
1768            if ([[searchNode name] isEqualToString: name] && [searchNode isFolder])
1769            {
1770                node = searchNode;
1771                *stop = YES;
1772            }
1773        }];
1774    }
1775   
1776    //create new folder or file if it doesn't already exist
1777    if (!node)
1778    {
1779        NSString * path = [[parent path] stringByAppendingPathComponent: [parent name]];
1780        if (isFolder)
1781            node = [[FileListNode alloc] initWithFolderName: name path: path torrent: self];
1782        else
1783        {
1784            node = [[FileListNode alloc] initWithFileName: name path: path size: size index: index torrent: self];
1785            [flatFileList addObject: node];
1786        }
1787       
1788        [parent insertChild: node];
1789        [node release];
1790    }
1791   
1792    if (isFolder)
1793    {
1794        [node insertIndex: index withSize: size];
1795       
1796        [components removeObjectAtIndex: 0];
1797        [self insertPath: components forParent: node fileSize: size index: index flatList: flatFileList];
1798    }
1799}
1800
1801- (void) sortFileList: (NSMutableArray *) fileNodes
1802{
1803    NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey: @"name" ascending: YES selector: @selector(localizedStandardCompare:)];
1804    [fileNodes sortUsingDescriptors: [NSArray arrayWithObject: descriptor]];
1805   
1806    [fileNodes enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(FileListNode * node, NSUInteger idx, BOOL * stop) {
1807        if ([node isFolder])
1808            [self sortFileList: [node children]];
1809    }];
1810}
1811
1812- (void) startQueue
1813{
1814    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
1815}
1816
1817//status has been retained
1818- (void) completenessChange: (NSDictionary *) statusInfo
1819{
1820    fStat = tr_torrentStat(fHandle); //don't call update yet to avoid auto-stop
1821   
1822    switch ([[statusInfo objectForKey: @"Status"] intValue])
1823    {
1824        case TR_SEED:
1825        case TR_PARTIAL_SEED:
1826            //simpler to create a new dictionary than to use statusInfo - avoids retention chicanery
1827            [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedDownloading" object: self
1828                userInfo: [NSDictionary dictionaryWithObject: [statusInfo objectForKey: @"WasRunning"] forKey: @"WasRunning"]];
1829           
1830            //quarantine the finished data
1831            NSString * dataLocation = [[self currentDirectory] stringByAppendingPathComponent: [self name]];
1832            FSRef ref;
1833            if (FSPathMakeRef((const UInt8 *)[dataLocation UTF8String], &ref, NULL) == noErr)
1834            {
1835                NSDictionary * quarantineProperties = [NSDictionary dictionaryWithObject: (NSString *)kLSQuarantineTypeOtherDownload forKey: (NSString *)kLSQuarantineTypeKey];
1836                if (LSSetItemAttribute(&ref, kLSRolesAll, kLSItemQuarantineProperties, quarantineProperties) != noErr)
1837                    NSLog(@"Failed to quarantine: %@", dataLocation);
1838            }
1839            else
1840                NSLog(@"Could not find file to quarantine: %@", dataLocation);
1841           
1842            break;
1843       
1844        case TR_LEECH:
1845            [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentRestartedDownloading" object: self];
1846            break;
1847    }
1848    [statusInfo release];
1849   
1850    [self update];
1851    [self updateTimeMachineExclude];
1852}
1853
1854- (void) ratioLimitHit
1855{
1856    fStat = tr_torrentStat(fHandle);
1857   
1858    [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedSeeding" object: self];
1859}
1860
1861- (void) idleLimitHit
1862{
1863    fStat = tr_torrentStat(fHandle);
1864   
1865    [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedSeeding" object: self];
1866}
1867
1868- (void) metadataRetrieved
1869{
1870    fStat = tr_torrentStat(fHandle);
1871   
1872    [self createFileList];
1873   
1874    [[NSNotificationCenter defaultCenter] postNotificationName: @"ResetInspector" object: self];
1875}
1876
1877- (BOOL) shouldShowEta
1878{
1879    if (fStat->activity == TR_STATUS_DOWNLOAD)
1880        return YES;
1881    else if ([self isSeeding])
1882    {
1883        //ratio: show if it's set at all
1884        if (tr_torrentGetSeedRatio(fHandle, NULL))
1885            return YES;
1886       
1887        //idle: show only if remaining time is less than cap
1888        if (fStat->etaIdle != TR_ETA_NOT_AVAIL && fStat->etaIdle < ETA_IDLE_DISPLAY_SEC)
1889            return YES;
1890    }
1891   
1892    return NO;
1893}
1894
1895- (NSString *) etaString
1896{
1897    NSInteger eta;
1898    BOOL fromIdle;
1899    //don't check for both, since if there's a regular ETA, the torrent isn't idle so it's meaningless
1900    if (fStat->eta != TR_ETA_NOT_AVAIL && fStat->eta != TR_ETA_UNKNOWN)
1901    {
1902        eta = fStat->eta;
1903        fromIdle = NO;
1904    }
1905    else if (fStat->etaIdle != TR_ETA_NOT_AVAIL && fStat->etaIdle < ETA_IDLE_DISPLAY_SEC)
1906    {
1907        eta = fStat->etaIdle;
1908        fromIdle = YES;
1909    }
1910    else
1911        return NSLocalizedString(@"remaining time unknown", "Torrent -> eta string");
1912   
1913    NSString * idleString = [NSString stringWithFormat: NSLocalizedString(@"%@ remaining", "Torrent -> eta string"),
1914                                [NSString timeString: eta showSeconds: YES maxFields: 2]];
1915    if (fromIdle)
1916        idleString = [idleString stringByAppendingFormat: @" (%@)", NSLocalizedString(@"inactive", "Torrent -> eta string")];
1917   
1918    return idleString;
1919}
1920
1921- (void) setTimeMachineExclude: (BOOL) exclude forPath: (NSString *) path
1922{
1923    CSBackupSetItemExcluded((CFURLRef)[NSURL fileURLWithPath: path], exclude, true);
1924}
1925
1926@end
Note: See TracBrowser for help on using the repository browser.