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