source: trunk/macosx/Torrent.m @ 13419

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

#4454 Patch by 0ptional: Time Machine exclusion for partial download does not work on Lion

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