source: trunk/macosx/Torrent.m @ 13334

Last change on this file since 13334 was 13334, checked in by livings124, 9 years ago

revert 13327 (#4924)

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