source: trunk/macosx/Torrent.m @ 13327

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

#4924 Use recycleURLs:completionHandler: to trash files

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