source: trunk/macosx/Torrent.m @ 12204

Last change on this file since 12204 was 12204, checked in by jordan, 11 years ago

(trunk) #4138 "use stdbool.h instead of tr_bool" -- done.

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