source: trunk/macosx/Torrent.m @ 7153

Last change on this file since 7153 was 7153, checked in by livings124, 12 years ago

#1029 show a warning dialog when removing a transfer's data if its directory contains extra files

  • Property svn:keywords set to Date Rev Author Id
File size: 66.4 KB
Line 
1/******************************************************************************
2 * $Id: Torrent.m 7153 2008-11-25 03:04:17Z livings124 $
3 *
4 * Copyright (c) 2006-2008 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 "NSApplicationAdditions.h"
29#import "NSStringAdditions.h"
30#import "utils.h" //tr_httpIsValidURL
31
32@interface Torrent (Private)
33
34- (id) initWithHash: (NSString *) hashString path: (NSString *) path torrentStruct: (tr_torrent *) torrentStruct lib: (tr_handle *) lib
35        publicTorrent: (NSNumber *) publicTorrent
36        downloadFolder: (NSString *) downloadFolder
37        useIncompleteFolder: (NSNumber *) useIncompleteFolder incompleteFolder: (NSString *) incompleteFolder
38        ratioSetting: (NSNumber *) ratioSetting ratioLimit: (NSNumber *) ratioLimit
39        waitToStart: (NSNumber *) waitToStart
40        orderValue: (NSNumber *) orderValue groupValue: (NSNumber *) groupValue addedTrackers: (NSNumber *) addedTrackers;
41
42- (BOOL) shouldUseIncompleteFolderForName: (NSString *) name;
43- (void) updateDownloadFolder;
44
45- (void) createFileList;
46- (void) insertPath: (NSMutableArray *) components forParent: (FileListNode *) parent fileSize: (uint64_t) size index: (NSInteger) index;
47
48- (void) completenessChange: (NSNumber *) status;
49
50- (void) quickPause;
51- (void) endQuickPause;
52
53- (NSString *) etaString: (NSInteger) eta;
54
55- (void) updateAllTrackers: (NSMutableArray *) trackers;
56
57- (void) trashFile: (NSString *) path;
58
59- (void) setTimeMachineExclude: (BOOL) exclude forPath: (NSString *) path;
60
61@end
62
63void completenessChangeCallback(tr_torrent * torrent, tr_completeness status, void * torrentData)
64{
65    [(Torrent *)torrentData performSelectorOnMainThread: @selector(completenessChange:)
66                withObject: [[NSNumber alloc] initWithInt: status] waitUntilDone: NO];
67}
68
69@implementation Torrent
70
71- (id) initWithPath: (NSString *) path location: (NSString *) location deleteTorrentFile: (torrentFileState) torrentDelete
72        lib: (tr_handle *) lib
73{
74    self = [self initWithHash: nil path: path torrentStruct: NULL lib: lib
75            publicTorrent: torrentDelete != TORRENT_FILE_DEFAULT ? [NSNumber numberWithBool: torrentDelete == TORRENT_FILE_SAVE] : nil
76            downloadFolder: location
77            useIncompleteFolder: nil incompleteFolder: nil
78            ratioSetting: nil ratioLimit: nil
79            waitToStart: nil orderValue: nil groupValue: nil addedTrackers: nil];
80   
81    if (self)
82    {
83        //if the public and private torrent files are the same, then there is no public torrent
84        if ([[self torrentLocation] isEqualToString: path])
85        {
86            fPublicTorrent = NO;
87            [fPublicTorrentLocation release];
88            fPublicTorrentLocation = nil;
89        }
90        else if (!fPublicTorrent)
91            [self trashFile: path];
92        else;
93    }
94    return self;
95}
96
97- (id) initWithTorrentStruct: (tr_torrent *) torrentStruct location: (NSString *) location lib: (tr_handle *) lib
98{
99    self = [self initWithHash: nil path: nil torrentStruct: torrentStruct lib: lib
100            publicTorrent: [NSNumber numberWithBool: NO]
101            downloadFolder: location
102            useIncompleteFolder: nil incompleteFolder: nil
103            ratioSetting: nil ratioLimit: nil
104            waitToStart: nil orderValue: nil groupValue: nil addedTrackers: nil];
105   
106    return self;
107}
108
109- (id) initWithHistory: (NSDictionary *) history lib: (tr_handle *) lib
110{
111    self = [self initWithHash: [history objectForKey: @"TorrentHash"]
112                path: [history objectForKey: @"TorrentPath"] torrentStruct: NULL lib: lib
113                publicTorrent: [history objectForKey: @"PublicCopy"]
114                downloadFolder: [history objectForKey: @"DownloadFolder"]
115                useIncompleteFolder: [history objectForKey: @"UseIncompleteFolder"]
116                incompleteFolder: [history objectForKey: @"IncompleteFolder"]
117                ratioSetting: [history objectForKey: @"RatioSetting"]
118                ratioLimit: [history objectForKey: @"RatioLimit"]
119                waitToStart: [history objectForKey: @"WaitToStart"]
120                orderValue: [history objectForKey: @"OrderValue"]
121                groupValue: [history objectForKey: @"GroupValue"]
122                addedTrackers: [history objectForKey: @"AddedTrackers"]];
123   
124    if (self)
125    {
126        //start transfer
127        NSNumber * active;
128        if ((active = [history objectForKey: @"Active"]) && [active boolValue])
129        {
130            fStat = tr_torrentStat(fHandle);
131            [self startTransfer];
132        }
133       
134        //upgrading from versions < 1.30: get old added, activity, and done dates
135        NSDate * date;
136        if ((date = [history objectForKey: @"Date"]))
137            tr_torrentSetAddedDate(fHandle, [date timeIntervalSince1970]);
138        if ((date = [history objectForKey: @"DateActivity"]))
139            tr_torrentSetActivityDate(fHandle, [date timeIntervalSince1970]);
140        if ((date = [history objectForKey: @"DateCompleted"]))
141            tr_torrentSetDoneDate(fHandle, [date timeIntervalSince1970]);
142    }
143    return self;
144}
145
146- (NSDictionary *) history
147{
148    NSMutableDictionary * history = [NSMutableDictionary dictionaryWithObjectsAndKeys:
149                    [NSNumber numberWithBool: fPublicTorrent], @"PublicCopy",
150                    [self hashString], @"TorrentHash",
151                    fDownloadFolder, @"DownloadFolder",
152                    [NSNumber numberWithBool: fUseIncompleteFolder], @"UseIncompleteFolder",
153                    [NSNumber numberWithBool: [self isActive]], @"Active",
154                    [NSNumber numberWithInt: fRatioSetting], @"RatioSetting",
155                    [NSNumber numberWithFloat: fRatioLimit], @"RatioLimit",
156                    [NSNumber numberWithBool: fWaitToStart], @"WaitToStart",
157                    [NSNumber numberWithInt: fOrderValue], @"OrderValue",
158                    [NSNumber numberWithInt: fGroupValue], @"GroupValue",
159                    [NSNumber numberWithBool: fAddedTrackers], @"AddedTrackers", nil];
160   
161    if (fIncompleteFolder)
162        [history setObject: fIncompleteFolder forKey: @"IncompleteFolder"];
163
164    if (fPublicTorrent)
165        [history setObject: [self publicTorrentLocation] forKey: @"TorrentPath"];
166       
167    return history;
168}
169
170- (void) dealloc
171{
172    [[NSNotificationCenter defaultCenter] removeObserver: self];
173   
174    if (fFileStat)
175        tr_torrentFilesFree(fFileStat, [self fileCount]);
176   
177    [fPreviousFinishedIndexes release];
178    [fPreviousFinishedIndexesDate release];
179   
180    [fNameString release];
181    [fHashString release];
182   
183    [fDownloadFolder release];
184    [fIncompleteFolder release];
185   
186    [fPublicTorrentLocation release];
187   
188    [fIcon release];
189   
190    [fFileList release];
191   
192    [fQuickPauseDict release];
193   
194    [super dealloc];
195}
196
197- (NSString *) description
198{
199    return [@"Torrent: " stringByAppendingString: [self name]];
200}
201
202- (void) closeRemoveTorrent
203{
204    //allow the file to be index by Time Machine
205    [self setTimeMachineExclude: NO forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
206   
207    tr_torrentRemove(fHandle);
208}
209
210- (void) changeIncompleteDownloadFolder: (NSString *) folder
211{
212    fUseIncompleteFolder = folder != nil;
213   
214    [fIncompleteFolder release];
215    fIncompleteFolder = fUseIncompleteFolder ? [folder retain] : nil;
216   
217    [self updateDownloadFolder];
218}
219
220- (void) changeDownloadFolder: (NSString *) folder
221{
222    [fDownloadFolder release];
223    fDownloadFolder = [folder retain];
224   
225    [self updateDownloadFolder];
226}
227
228- (NSString *) downloadFolder
229{
230    return [NSString stringWithUTF8String: tr_torrentGetDownloadDir(fHandle)];
231}
232
233- (void) getAvailability: (int8_t *) tab size: (NSInteger) size
234{
235    tr_torrentAvailability(fHandle, tab, size);
236}
237
238- (void) getAmountFinished: (float *) tab size: (NSInteger) size
239{
240    tr_torrentAmountFinished(fHandle, tab, size);
241}
242
243- (NSIndexSet *) previousFinishedPieces
244{
245    //if the torrent hasn't been seen in a bit, and therefore hasn't been refreshed, return nil
246    if (fPreviousFinishedIndexesDate && [fPreviousFinishedIndexesDate timeIntervalSinceNow] > -2.0)
247        return fPreviousFinishedIndexes;
248    else
249        return nil;
250}
251
252-(void) setPreviousFinishedPieces: (NSIndexSet *) indexes
253{
254    [fPreviousFinishedIndexes release];
255    fPreviousFinishedIndexes = [indexes retain];
256   
257    [fPreviousFinishedIndexesDate release];
258    fPreviousFinishedIndexesDate = indexes != nil ? [[NSDate alloc] init] : nil;
259}
260
261- (void) update
262{
263    //get previous status values before update
264    BOOL wasChecking = NO, wasError = NO, wasStalled = NO;
265    if (fStat != NULL)
266    {
267        wasChecking = [self isChecking];
268        wasError = [self isError];
269        wasStalled = fStalled;
270    }
271   
272    fStat = tr_torrentStat(fHandle);
273   
274    //check to stop for ratio
275    CGFloat stopRatio;
276    if ([self isSeeding] && (stopRatio = [self actualStopRatio]) != INVALID && [self ratio] >= stopRatio)
277    {
278        [self setRatioSetting: NSOffState];
279        [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentStoppedForRatio" object: self];
280       
281        [self stopTransfer];
282        fStat = tr_torrentStat(fHandle);
283       
284        fFinishedSeeding = YES;
285    }
286   
287    //check if stalled (stored because based on time and needs to check if it was previously stalled)
288    fStalled = [self isActive] && [fDefaults boolForKey: @"CheckStalled"]
289                && [self stalledMinutes] > [fDefaults integerForKey: @"StalledMinutes"];
290   
291    //update queue for checking (from downloading to seeding), stalled, or error
292    if ((wasChecking && ![self isChecking]) || (wasStalled != fStalled) || (!wasError && [self isError] && [self isActive]))
293        [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
294}
295
296- (void) startTransfer
297{
298    fWaitToStart = NO;
299    fFinishedSeeding = NO;
300   
301    if (![self isActive] && [self alertForFolderAvailable] && [self alertForRemainingDiskSpace])
302    {
303        tr_torrentStart(fHandle);
304        [self update];
305    }
306}
307
308- (void) stopTransfer
309{
310    fWaitToStart = NO;
311   
312    if ([self isActive])
313    {
314        tr_torrentStop(fHandle);
315        [self update];
316       
317        [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
318    }
319}
320
321- (void) sleep
322{
323    if ((fResumeOnWake = [self isActive]))
324        tr_torrentStop(fHandle);
325}
326
327- (void) wakeUp
328{
329    if (fResumeOnWake)
330        tr_torrentStart(fHandle);
331}
332
333- (void) manualAnnounce
334{
335    tr_torrentManualUpdate(fHandle);
336}
337
338- (BOOL) canManualAnnounce
339{
340    return tr_torrentCanManualUpdate(fHandle);
341}
342
343- (void) resetCache
344{
345    tr_torrentVerify(fHandle);
346    [self update];
347}
348
349- (CGFloat) ratio
350{
351    return fStat->ratio;
352}
353
354- (NSInteger) ratioSetting
355{
356    return fRatioSetting;
357}
358
359- (void) setRatioSetting: (NSInteger) setting
360{
361    fRatioSetting = setting;
362}
363
364- (CGFloat) ratioLimit
365{
366    return fRatioLimit;
367}
368
369- (void) setRatioLimit: (CGFloat) limit
370{
371    if (limit >= 0)
372        fRatioLimit = limit;
373}
374
375- (CGFloat) actualStopRatio
376{
377    if (fRatioSetting == NSOnState)
378        return fRatioLimit;
379    else if (fRatioSetting == NSMixedState && [fDefaults boolForKey: @"RatioCheck"])
380        return [fDefaults floatForKey: @"RatioLimit"];
381    else
382        return INVALID;
383}
384
385- (CGFloat) progressStopRatio
386{
387    CGFloat stopRatio, ratio;
388    if ((stopRatio = [self actualStopRatio]) == INVALID || (ratio = [self ratio]) >= stopRatio)
389        return 1.0;
390    else if (stopRatio > 0.0)
391        return ratio / stopRatio;
392    else
393        return 0.0;
394}
395
396- (tr_speedlimit) speedMode: (BOOL) upload
397{
398    return tr_torrentGetSpeedMode(fHandle, upload ? TR_UP : TR_DOWN);
399}
400
401- (void) setSpeedMode: (tr_speedlimit) mode upload: (BOOL) upload
402{
403    tr_torrentSetSpeedMode(fHandle, upload ? TR_UP : TR_DOWN, mode);
404}
405
406- (NSInteger) speedLimit: (BOOL) upload
407{
408    return tr_torrentGetSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN);
409}
410
411- (void) setSpeedLimit: (NSInteger) limit upload: (BOOL) upload
412{
413    tr_torrentSetSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN, limit);
414}
415
416- (void) setMaxPeerConnect: (uint16_t) count
417{
418    NSAssert(count > 0, @"max peer count must be greater than 0");
419   
420    tr_torrentSetPeerLimit(fHandle, count);
421}
422
423- (uint16_t) maxPeerConnect
424{
425    return tr_torrentGetPeerLimit(fHandle);
426}
427
428- (void) setWaitToStart: (BOOL) wait
429{
430    fWaitToStart = wait;
431}
432
433- (BOOL) waitingToStart
434{
435    return fWaitToStart;
436}
437
438- (void) revealData
439{
440    [[NSWorkspace sharedWorkspace] selectFile: [self dataLocation] inFileViewerRootedAtPath: nil];
441}
442
443- (void) revealPublicTorrent
444{
445    if (fPublicTorrent)
446        [[NSWorkspace sharedWorkspace] selectFile: fPublicTorrentLocation inFileViewerRootedAtPath: nil];
447}
448
449- (void) trashData
450{
451    if ([self isFolder] && [fDefaults boolForKey: @"WarningCheckContentsForRemove"])
452    {
453        NSEnumerator * enumerator = [[NSFileManager defaultManager] enumeratorAtPath: [self dataLocation]];
454        NSString * file;
455        while ((file = [enumerator nextObject]))
456        {
457            if ([[file lastPathComponent] hasPrefix: @"."])
458                continue;
459           
460            file = [[self name] stringByAppendingPathComponent: file];
461            BOOL isExtra = YES;
462       
463            NSEnumerator * nodeEnumerator = [fFileList objectEnumerator];
464            FileListNode * node;
465            while ((node = [nodeEnumerator nextObject]))
466            {
467                if ([node containsPath: file])
468                {
469                    isExtra = NO;
470                    break;
471                }
472            }
473           
474            if (!isExtra)
475                continue;
476
477            NSLog(@"Extra file found: %@", file);
478           
479            NSAlert * alert = [[NSAlert alloc] init];
480            [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"\"%@\" contains extra content.",
481                                                        "Delete folder with extra contents -> title"), [self name]]];
482            [alert setInformativeText: NSLocalizedString(@"The directory contains data that is not part of the transfer."
483                " Are you sure you want to move this directory to the trash?", "Delete folder with extra contents -> message")];
484            [alert addButtonWithTitle: NSLocalizedString(@"Remove", "Delete folder with extra contents -> button")];
485            [alert addButtonWithTitle: NSLocalizedString(@"Keep", "Delete folder with extra contents -> button")];
486           
487            BOOL onLeopard = [NSApp isOnLeopardOrBetter];
488            if (onLeopard)
489            {
490                [alert setShowsSuppressionButton: YES];
491                [[alert suppressionButton] setTitle: NSLocalizedString(@"Do not check directory contents again",
492                                                        "Delete folder with extra contents -> button")];
493            }
494            else
495                [alert addButtonWithTitle: NSLocalizedString(@"Never Check", "Delete folder with extra contents -> button")];
496
497            NSInteger result = [alert runModal];
498            if ((onLeopard ? [[alert suppressionButton] state] == NSOnState : result == NSAlertThirdButtonReturn))
499                [fDefaults setBool: NO forKey: @"WarningCheckContentsForRemove"];
500            [alert release];
501           
502            if (result == NSAlertSecondButtonReturn)
503                return;
504            else
505                break;
506        }
507    }
508   
509    [self trashFile: [self dataLocation]];
510}
511
512- (void) trashTorrent
513{
514    if (fPublicTorrent)
515    {
516        [self trashFile: fPublicTorrentLocation];
517        [fPublicTorrentLocation release];
518        fPublicTorrentLocation = nil;
519       
520        fPublicTorrent = NO;
521    }
522}
523
524- (void) moveTorrentDataFileTo: (NSString *) folder
525{
526    NSString * oldFolder = [self downloadFolder];
527    if (![oldFolder isEqualToString: folder] || ![fDownloadFolder isEqualToString: folder])
528    {
529        //check if moving inside itself
530        NSArray * oldComponents = [oldFolder pathComponents],
531                * newComponents = [folder pathComponents];
532        NSInteger count;
533       
534        if ((count = [oldComponents count]) < [newComponents count]
535                && [[newComponents objectAtIndex: count] isEqualToString: [self name]]
536                && [oldComponents isEqualToArray:
537                        [newComponents objectsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, count)]]])
538        {
539            NSAlert * alert = [[NSAlert alloc] init];
540            [alert setMessageText: NSLocalizedString(@"A folder cannot be moved to inside itself.",
541                                                        "Move inside itself alert -> title")];
542            [alert setInformativeText: [NSString stringWithFormat:
543                            NSLocalizedString(@"The move operation of \"%@\" cannot be done.",
544                                                "Move inside itself alert -> message"), [self name]]];
545            [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move inside itself alert -> button")];
546           
547            [alert runModal];
548            [alert release];
549           
550            return;
551        }
552       
553        [self quickPause];
554       
555        //allow if file can be moved or does not exist
556        if ([[NSFileManager defaultManager] movePath: [oldFolder stringByAppendingPathComponent: [self name]]
557                            toPath: [folder stringByAppendingPathComponent: [self name]] handler: nil]
558            || ![[NSFileManager defaultManager] fileExistsAtPath: [oldFolder stringByAppendingPathComponent: [self name]]])
559        {
560            //get rid of both incomplete folder and old download folder, even if move failed
561            fUseIncompleteFolder = NO;
562            if (fIncompleteFolder)
563            {
564                [fIncompleteFolder release];
565                fIncompleteFolder = nil;
566            }
567            [self changeDownloadFolder: folder];
568           
569            [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateStats" object: nil];
570           
571            [self endQuickPause];
572        }
573        else
574        {
575            [self endQuickPause];
576       
577            NSAlert * alert = [[NSAlert alloc] init];
578            [alert setMessageText: NSLocalizedString(@"There was an error moving the data file.", "Move error alert -> title")];
579            [alert setInformativeText: [NSString stringWithFormat:
580                            NSLocalizedString(@"The move operation of \"%@\" cannot be done.",
581                                                "Move error alert -> message"), [self name]]];
582            [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move error alert -> button")];
583           
584            [alert runModal];
585            [alert release];
586        }
587    }
588}
589
590- (void) copyTorrentFileTo: (NSString *) path
591{
592    [[NSFileManager defaultManager] copyPath: [self torrentLocation] toPath: path handler: nil];
593}
594
595- (BOOL) alertForRemainingDiskSpace
596{
597    if ([self allDownloaded] || ![fDefaults boolForKey: @"WarningRemainingSpace"])
598        return YES;
599   
600    NSFileManager * fileManager = [NSFileManager defaultManager];
601    NSString * downloadFolder = [self downloadFolder];
602   
603    NSString * volumeName;
604    if ((volumeName = [[fileManager componentsToDisplayForPath: downloadFolder] objectAtIndex: 0]))
605    {
606        BOOL onLeopard = [NSApp isOnLeopardOrBetter];
607       
608        NSDictionary * systemAttributes = onLeopard ? [fileManager attributesOfFileSystemForPath: downloadFolder error: NULL]
609                                            : [fileManager fileSystemAttributesAtPath: downloadFolder];
610        uint64_t remainingSpace = [[systemAttributes objectForKey: NSFileSystemFreeSize] unsignedLongLongValue];
611       
612        //if the remaining space is greater than the size left, then there is enough space regardless of preallocation
613        if (remainingSpace < [self sizeLeft] && remainingSpace < tr_torrentGetBytesLeftToAllocate(fHandle))
614        {
615            NSAlert * alert = [[NSAlert alloc] init];
616            [alert setMessageText: [NSString stringWithFormat:
617                                    NSLocalizedString(@"Not enough remaining disk space to download \"%@\" completely.",
618                                        "Torrent disk space alert -> title"), [self name]]];
619            [alert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"The transfer will be paused."
620                                        " Clear up space on %@ or deselect files in the torrent inspector to continue.",
621                                        "Torrent disk space alert -> message"), volumeName]];
622            [alert addButtonWithTitle: NSLocalizedString(@"OK", "Torrent disk space alert -> button")];
623            [alert addButtonWithTitle: NSLocalizedString(@"Download Anyway", "Torrent disk space alert -> button")];
624           
625            if (onLeopard)
626            {
627                [alert setShowsSuppressionButton: YES];
628                [[alert suppressionButton] setTitle: NSLocalizedString(@"Do not check disk space again",
629                                                        "Torrent disk space alert -> button")];
630            }
631            else
632                [alert addButtonWithTitle: NSLocalizedString(@"Always Download", "Torrent disk space alert -> button")];
633
634            NSInteger result = [alert runModal];
635            if ((onLeopard ? [[alert suppressionButton] state] == NSOnState : result == NSAlertThirdButtonReturn))
636                [fDefaults setBool: NO forKey: @"WarningRemainingSpace"];
637            [alert release];
638           
639            return result != NSAlertFirstButtonReturn;
640        }
641    }
642    return YES;
643}
644
645- (BOOL) alertForFolderAvailable
646{
647    #warning check for change from incomplete to download folder first
648    if (access(tr_torrentGetDownloadDir(fHandle), 0))
649    {
650        NSAlert * alert = [[NSAlert alloc] init];
651        [alert setMessageText: [NSString stringWithFormat:
652                                NSLocalizedString(@"The folder for downloading \"%@\" cannot be used.",
653                                    "Folder cannot be used alert -> title"), [self name]]];
654        [alert setInformativeText: [NSString stringWithFormat:
655                        NSLocalizedString(@"\"%@\" cannot be used. The transfer will be paused.",
656                                            "Folder cannot be used alert -> message"), [self downloadFolder]]];
657        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Folder cannot be used alert -> button")];
658        [alert addButtonWithTitle: [NSLocalizedString(@"Choose New Location",
659                                    "Folder cannot be used alert -> location button") stringByAppendingEllipsis]];
660       
661        if ([alert runModal] != NSAlertFirstButtonReturn)
662        {
663            NSOpenPanel * panel = [NSOpenPanel openPanel];
664           
665            [panel setPrompt: NSLocalizedString(@"Select", "Folder cannot be used alert -> prompt")];
666            [panel setAllowsMultipleSelection: NO];
667            [panel setCanChooseFiles: NO];
668            [panel setCanChooseDirectories: YES];
669            [panel setCanCreateDirectories: YES];
670
671            [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the download folder for \"%@\"",
672                                "Folder cannot be used alert -> select destination folder"), [self name]]];
673           
674            [[NSNotificationCenter defaultCenter] postNotificationName: @"MakeWindowKey" object: nil];
675            [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: [NSApp keyWindow] modalDelegate: self
676                    didEndSelector: @selector(destinationChoiceClosed:returnCode:contextInfo:) contextInfo: nil];
677        }
678       
679        [alert release];
680       
681        return NO;
682    }
683    return YES;
684}
685
686- (void) destinationChoiceClosed: (NSOpenPanel *) openPanel returnCode: (NSInteger) code contextInfo: (void *) context
687{
688    if (code != NSOKButton)
689        return;
690   
691    [self changeDownloadFolder: [[openPanel filenames] objectAtIndex: 0]];
692   
693    [self startTransfer];
694    [self update];
695   
696    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateStats" object: nil];
697}
698
699- (BOOL) alertForMoveFolderAvailable
700{
701    if (access([fDownloadFolder UTF8String], 0))
702    {
703        NSAlert * alert = [[NSAlert alloc] init];
704        [alert setMessageText: [NSString stringWithFormat:
705                                NSLocalizedString(@"The folder for moving the completed \"%@\" cannot be used.",
706                                    "Move folder cannot be used alert -> title"), [self name]]];
707        [alert setInformativeText: [NSString stringWithFormat:
708                                NSLocalizedString(@"\"%@\" cannot be used. The file will remain in its current location.",
709                                    "Move folder cannot be used alert -> message"), fDownloadFolder]];
710        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move folder cannot be used alert -> button")];
711       
712        [alert runModal];
713        [alert release];
714       
715        return NO;
716    }
717   
718    return YES;
719}
720
721- (NSImage *) icon
722{
723    if (!fIcon)
724    {
725        fIcon = [[[NSWorkspace sharedWorkspace] iconForFileType: [self isFolder] ? NSFileTypeForHFSTypeCode('fldr')
726                                                : [[self name] pathExtension]] retain];
727        [fIcon setFlipped: YES];
728    }
729    return fIcon;
730}
731
732- (NSString *) name
733{
734    return fNameString;
735}
736
737- (BOOL) isFolder
738{
739    return fInfo->isMultifile;
740}
741
742- (uint64_t) size
743{
744    return fInfo->totalSize;
745}
746
747- (uint64_t) sizeLeft
748{
749    return fStat->leftUntilDone;
750}
751
752- (NSString *) trackerAddressAnnounce
753{
754    return fStat->announceURL ? [NSString stringWithUTF8String: fStat->announceURL] : nil;
755}
756
757- (NSDate *) lastAnnounceTime
758{
759    NSInteger date = fStat->lastAnnounceTime;
760    return date > 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
761}
762
763- (NSInteger) nextAnnounceTime
764{
765    NSInteger date = fStat->nextAnnounceTime;
766    NSTimeInterval difference;
767    switch (date)
768    {
769        case 0:
770            return STAT_TIME_NONE;
771        case 1:
772            return STAT_TIME_NOW;
773        default:
774            difference = [[NSDate dateWithTimeIntervalSince1970: date] timeIntervalSinceNow];
775            return difference > 0 ? (NSInteger)difference : STAT_TIME_NONE;
776    }
777}
778
779- (NSString *) announceResponse
780{
781    return [NSString stringWithUTF8String: fStat->announceResponse];
782}
783
784- (NSString *) trackerAddressScrape
785{
786    return fStat->scrapeURL ? [NSString stringWithUTF8String: fStat->scrapeURL] : nil;
787}
788
789- (NSDate *) lastScrapeTime
790{
791    NSInteger date = fStat->lastScrapeTime;
792    return date > 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
793}
794
795- (NSInteger) nextScrapeTime
796{
797    NSInteger date = fStat->nextScrapeTime;
798    NSTimeInterval difference;
799    switch (date)
800    {
801        case 0:
802            return STAT_TIME_NONE;
803        case 1:
804            return STAT_TIME_NOW;
805        default:
806            difference = [[NSDate dateWithTimeIntervalSince1970: date] timeIntervalSinceNow];
807            return difference > 0 ? (NSInteger)difference : STAT_TIME_NONE;
808    }
809}
810
811- (NSString *) scrapeResponse
812{
813    return [NSString stringWithUTF8String: fStat->scrapeResponse];
814}
815
816- (NSMutableArray *) allTrackers: (BOOL) separators
817{
818    NSInteger count = fInfo->trackerCount, capacity = count;
819    if (separators)
820        capacity += fInfo->trackers[count-1].tier + 1;
821    NSMutableArray * allTrackers = [NSMutableArray arrayWithCapacity: capacity];
822   
823    for (NSInteger i = 0, tier = -1; i < count; i++)
824    {
825        if (separators && tier != fInfo->trackers[i].tier)
826        {
827            tier = fInfo->trackers[i].tier;
828            [allTrackers addObject: [NSNumber numberWithInt: fAddedTrackers ? tier : tier + 1]];
829        }
830       
831        [allTrackers addObject: [NSString stringWithUTF8String: fInfo->trackers[i].announce]];
832    }
833   
834    return allTrackers;
835}
836
837- (BOOL) updateAllTrackersForAdd: (NSMutableArray *) trackers
838{
839    //find added tracker at end of first tier
840    NSInteger i;
841    for (i = 1; i < [trackers count]; i++)
842        if ([[trackers objectAtIndex: i] isKindOfClass: [NSNumber class]])
843            break;
844    i--;
845   
846    NSString * tracker = [trackers objectAtIndex: i];
847   
848    tracker = [tracker stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
849   
850    if ([tracker rangeOfString: @"://"].location == NSNotFound)
851    {
852        tracker = [@"http://" stringByAppendingString: tracker];
853        [trackers replaceObjectAtIndex: i withObject: tracker];
854    }
855   
856    if (!tr_httpIsValidURL([tracker UTF8String]))
857        return NO;
858   
859    [self updateAllTrackers: trackers];
860   
861    fAddedTrackers = YES;
862    return YES;
863}
864
865- (void) updateAllTrackersForRemove: (NSMutableArray *) trackers
866{
867    //check if no user-added groups
868    if ([[trackers objectAtIndex: 0] intValue] != 0)
869        fAddedTrackers = NO;
870   
871    [self updateAllTrackers: trackers];
872}
873
874- (BOOL) hasAddedTrackers
875{
876    return fAddedTrackers;
877}
878
879- (NSString *) comment
880{
881    return [NSString stringWithUTF8String: fInfo->comment];
882}
883
884- (NSString *) creator
885{
886    return [NSString stringWithUTF8String: fInfo->creator];
887}
888
889- (NSDate *) dateCreated
890{
891    NSInteger date = fInfo->dateCreated;
892    return date > 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
893}
894
895- (NSInteger) pieceSize
896{
897    return fInfo->pieceSize;
898}
899
900- (NSInteger) pieceCount
901{
902    return fInfo->pieceCount;
903}
904
905- (NSString *) hashString
906{
907    return fHashString;
908}
909
910- (BOOL) privateTorrent
911{
912    return fInfo->isPrivate;
913}
914
915- (NSString *) torrentLocation
916{
917    return [NSString stringWithUTF8String: fInfo->torrent];
918}
919
920- (NSString *) publicTorrentLocation
921{
922    return fPublicTorrentLocation;
923}
924
925- (NSString *) dataLocation
926{
927    return [[self downloadFolder] stringByAppendingPathComponent: [self name]];
928}
929
930- (BOOL) publicTorrent
931{
932    return fPublicTorrent;
933}
934
935- (CGFloat) progress
936{
937    return fStat->percentComplete;
938}
939
940- (CGFloat) progressDone
941{
942    return fStat->percentDone;
943}
944
945- (CGFloat) progressLeft
946{
947    return (CGFloat)[self sizeLeft] / [self size];
948}
949
950- (CGFloat) checkingProgress
951{
952    return fStat->recheckProgress;
953}
954
955- (NSInteger) eta
956{
957    return fStat->eta;
958}
959
960- (NSInteger) etaRatio
961{
962    if (![self isSeeding])
963        return TR_ETA_UNKNOWN;
964   
965    CGFloat uploadRate = [self uploadRate];
966    if (uploadRate < 0.1)
967        return TR_ETA_UNKNOWN;
968   
969    CGFloat stopRatio = [self actualStopRatio], ratio = [self ratio];
970    if (stopRatio == INVALID || ratio >= stopRatio)
971        return TR_ETA_UNKNOWN;
972   
973    CGFloat haveDownloaded = (CGFloat)([self downloadedTotal] > 0 ? [self downloadedTotal] : [self haveVerified]);
974    CGFloat needUploaded = haveDownloaded * (stopRatio - ratio);
975    return needUploaded / uploadRate / 1024.0;
976}
977
978- (CGFloat) notAvailableDesired
979{
980    return 1.0 - (CGFloat)fStat->desiredAvailable / [self sizeLeft];
981}
982
983- (BOOL) isActive
984{
985    return fStat->activity != TR_STATUS_STOPPED;
986}
987
988- (BOOL) isSeeding
989{
990    return fStat->activity == TR_STATUS_SEED;
991}
992
993- (BOOL) isChecking
994{
995    return fStat->activity == TR_STATUS_CHECK || fStat->activity == TR_STATUS_CHECK_WAIT;
996}
997
998- (BOOL) isCheckingWaiting
999{
1000    return fStat->activity == TR_STATUS_CHECK_WAIT;
1001}
1002
1003- (BOOL) allDownloaded
1004{
1005    return [self progressDone] >= 1.0;
1006}
1007
1008- (BOOL) isComplete
1009{
1010    return [self progress] >= 1.0;
1011}
1012
1013- (BOOL) isError
1014{
1015    return fStat->error != TR_OK;
1016}
1017
1018- (NSString *) errorMessage
1019{
1020    if (![self isError])
1021        return @"";
1022   
1023    NSString * error;
1024    if (!(error = [NSString stringWithUTF8String: fStat->errorString])
1025        && !(error = [NSString stringWithCString: fStat->errorString encoding: NSISOLatin1StringEncoding]))
1026        error = [NSString stringWithFormat: @"(%@)", NSLocalizedString(@"unreadable error", "Torrent -> error string unreadable")];
1027   
1028    return error;
1029}
1030
1031- (NSArray *) peers
1032{
1033    int totalPeers;
1034    tr_peer_stat * peers = tr_torrentPeers(fHandle, &totalPeers);
1035   
1036    NSMutableArray * peerDicts = [NSMutableArray arrayWithCapacity: totalPeers];
1037   
1038    for (int i = 0; i < totalPeers; i++)
1039    {
1040        tr_peer_stat * peer = &peers[i];
1041        NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity: 9];
1042       
1043        [dict setObject: [NSNumber numberWithInt: peer->from] forKey: @"From"];
1044        [dict setObject: [NSString stringWithUTF8String: peer->addr] forKey: @"IP"];
1045        [dict setObject: [NSNumber numberWithInt: peer->port] forKey: @"Port"];
1046        [dict setObject: [NSNumber numberWithFloat: peer->progress] forKey: @"Progress"];
1047        [dict setObject: [NSNumber numberWithBool: peer->isEncrypted] forKey: @"Encryption"];
1048        [dict setObject: [NSString stringWithUTF8String: peer->client] forKey: @"Client"];
1049        [dict setObject: [NSString stringWithUTF8String: peer->flagStr] forKey: @"Flags"];
1050       
1051        if (peer->isUploadingTo)
1052            [dict setObject: [NSNumber numberWithFloat: peer->rateToPeer] forKey: @"UL To Rate"];
1053        if (peer->isDownloadingFrom)
1054            [dict setObject: [NSNumber numberWithFloat: peer->rateToClient] forKey: @"DL From Rate"];
1055       
1056        [peerDicts addObject: dict];
1057    }
1058   
1059    tr_torrentPeersFree(peers, totalPeers);
1060   
1061    return peerDicts;
1062}
1063
1064- (NSUInteger) webSeedCount
1065{
1066    return fInfo->webseedCount;
1067}
1068
1069- (NSArray *) webSeeds
1070{
1071    const NSInteger webSeedCount = fInfo->webseedCount;
1072    NSMutableArray * webSeeds = [NSMutableArray arrayWithCapacity: webSeedCount];
1073   
1074    float * dlSpeeds = tr_torrentWebSpeeds(fHandle);
1075   
1076    for (NSInteger i = 0; i < webSeedCount; i++)
1077    {
1078        NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity: 2];
1079       
1080        [dict setObject: [NSString stringWithUTF8String: fInfo->webseeds[i]] forKey: @"Address"];
1081       
1082        if (dlSpeeds[i] != -1.0)
1083            [dict setObject: [NSNumber numberWithFloat: dlSpeeds[i]] forKey: @"DL From Rate"];
1084       
1085        [webSeeds addObject: dict];
1086    }
1087   
1088    tr_free(dlSpeeds);
1089   
1090    return webSeeds;
1091}
1092
1093- (NSString *) progressString
1094{
1095    NSString * string;
1096   
1097    if (![self allDownloaded])
1098    {
1099        CGFloat progress;
1100        if ([self isFolder] && [fDefaults boolForKey: @"DisplayStatusProgressSelected"])
1101        {
1102            string = [NSString stringWithFormat: NSLocalizedString(@"%@ of %@ selected", "Torrent -> progress string"),
1103                        [NSString stringForFileSize: [self haveTotal]], [NSString stringForFileSize: [self totalSizeSelected]]];
1104            progress = 100.0 * [self progressDone];
1105        }
1106        else
1107        {
1108            string = [NSString stringWithFormat: NSLocalizedString(@"%@ of %@", "Torrent -> progress string"),
1109                        [NSString stringForFileSize: [self haveTotal]], [NSString stringForFileSize: [self size]]];
1110            progress = 100.0 * [self progress];
1111        }
1112       
1113        string = [NSString localizedStringWithFormat: @"%@ (%.2f%%)", string, progress];
1114    }
1115    else
1116    {
1117        NSString * downloadString;
1118        if (![self isComplete]) //only multifile possible
1119        {
1120            if ([fDefaults boolForKey: @"DisplayStatusProgressSelected"])
1121                downloadString = [NSString stringWithFormat: NSLocalizedString(@"%@ selected", "Torrent -> progress string"),
1122                                    [NSString stringForFileSize: [self haveTotal]]];
1123            else
1124            {
1125                downloadString = [NSString stringWithFormat: NSLocalizedString(@"%@ of %@", "Torrent -> progress string"),
1126                                    [NSString stringForFileSize: [self haveTotal]], [NSString stringForFileSize: [self size]]];
1127               
1128                downloadString = [NSString localizedStringWithFormat: @"%@ (%.2f%%)", downloadString, 100.0 * [self progress]];
1129            }
1130        }
1131        else
1132            downloadString = [NSString stringForFileSize: [self size]];
1133       
1134        NSString * uploadString = [NSString stringWithFormat: NSLocalizedString(@"uploaded %@ (Ratio: %@)",
1135                                    "Torrent -> progress string"), [NSString stringForFileSize: [self uploadedTotal]],
1136                                    [NSString stringForRatio: [self ratio]]];
1137       
1138        string = [downloadString stringByAppendingFormat: @", %@", uploadString];
1139    }
1140   
1141    //add time when downloading
1142    if (fStat->activity == TR_STATUS_DOWNLOAD || ([self isSeeding]
1143        && (fRatioSetting == NSOnState || (fRatioSetting == NSMixedState && [fDefaults boolForKey: @"RatioCheck"]))))
1144    {
1145        NSInteger eta = fStat->activity == TR_STATUS_DOWNLOAD ? [self eta] : [self etaRatio];
1146        string = [string stringByAppendingFormat: @" - %@", [self etaString: eta]];
1147    }
1148   
1149    return string;
1150}
1151
1152- (NSString *) statusString
1153{
1154    NSString * string;
1155   
1156    if ([self isError])
1157    {
1158        string = NSLocalizedString(@"Error", "Torrent -> status string");
1159        NSString * errorString = [self errorMessage];
1160        if (errorString && ![errorString isEqualToString: @""])
1161            string = [string stringByAppendingFormat: @": %@", errorString];
1162    }
1163    else
1164    {
1165        switch (fStat->activity)
1166        {
1167            case TR_STATUS_STOPPED:
1168                if (fWaitToStart)
1169                {
1170                    string = ![self allDownloaded]
1171                            ? [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis]
1172                            : [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
1173                }
1174                else if (fFinishedSeeding)
1175                    string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1176                else
1177                    string = NSLocalizedString(@"Paused", "Torrent -> status string");
1178                break;
1179
1180            case TR_STATUS_CHECK_WAIT:
1181                string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1182                break;
1183
1184            case TR_STATUS_CHECK:
1185                string = [NSString localizedStringWithFormat: NSLocalizedString(@"Checking existing data (%.2f%%)",
1186                                        "Torrent -> status string"), 100.0 * [self checkingProgress]];
1187                break;
1188
1189            case TR_STATUS_DOWNLOAD:
1190                if ([self totalPeersConnected] != 1)
1191                    string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of %d peers",
1192                                                    "Torrent -> status string"), [self peersSendingToUs], [self totalPeersConnected]];
1193                else
1194                    string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of 1 peer",
1195                                                    "Torrent -> status string"), [self peersSendingToUs]];
1196               
1197                NSInteger webSeedCount = fStat->webseedsSendingToUs;
1198                if (webSeedCount > 0)
1199                {
1200                    NSString * webSeedString;
1201                    if (webSeedCount == 1)
1202                        webSeedString = NSLocalizedString(@"web seed", "Torrent -> status string");
1203                    else
1204                        webSeedString = [NSString stringWithFormat: NSLocalizedString(@"%d web seeds", "Torrent -> status string"),
1205                                                                    webSeedCount];
1206                   
1207                    string = [string stringByAppendingFormat: @" + %@", webSeedString];
1208                }
1209               
1210                break;
1211
1212            case TR_STATUS_SEED:
1213                if ([self totalPeersConnected] != 1)
1214                    string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of %d peers", "Torrent -> status string"),
1215                                                    [self peersGettingFromUs], [self totalPeersConnected]];
1216                else
1217                    string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of 1 peer", "Torrent -> status string"),
1218                                                    [self peersGettingFromUs]];
1219        }
1220       
1221        if (fStalled)
1222            string = [NSLocalizedString(@"Stalled", "Torrent -> status string") stringByAppendingFormat: @", %@", string];
1223    }
1224   
1225    //append even if error
1226    if ([self isActive] && ![self isChecking])
1227    {
1228        if (fStat->activity == TR_STATUS_DOWNLOAD)
1229            string = [string stringByAppendingFormat: @" - %@: %@, %@: %@",
1230                        NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]],
1231                        NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1232        else
1233            string = [string stringByAppendingFormat: @" - %@: %@",
1234                        NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1235    }
1236   
1237    return string;
1238}
1239
1240- (NSString *) shortStatusString
1241{
1242    NSString * string;
1243   
1244    switch (fStat->activity)
1245    {
1246        case TR_STATUS_STOPPED:
1247            if (fWaitToStart)
1248            {
1249                string = ![self allDownloaded]
1250                        ? [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis]
1251                        : [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
1252            }
1253            else if (fFinishedSeeding)
1254                string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1255            else
1256                string = NSLocalizedString(@"Paused", "Torrent -> status string");
1257            break;
1258
1259        case TR_STATUS_CHECK_WAIT:
1260            string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1261            break;
1262
1263        case TR_STATUS_CHECK:
1264            string = [NSString localizedStringWithFormat: NSLocalizedString(@"Checking existing data (%.2f%%)",
1265                                    "Torrent -> status string"), 100.0 * [self checkingProgress]];
1266            break;
1267       
1268        case TR_STATUS_DOWNLOAD:
1269            string = [NSString stringWithFormat: @"%@: %@, %@: %@",
1270                            NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]],
1271                            NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1272            break;
1273       
1274        case TR_STATUS_SEED:
1275            string = [NSString stringWithFormat: @"%@: %@, %@: %@",
1276                            NSLocalizedString(@"Ratio", "Torrent -> status string"), [NSString stringForRatio: [self ratio]],
1277                            NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1278    }
1279   
1280    return string;
1281}
1282
1283- (NSString *) remainingTimeString
1284{
1285    if (![self isActive] || ([self isSeeding]
1286        && !(fRatioSetting == NSOnState || (fRatioSetting == NSMixedState && [fDefaults boolForKey: @"RatioCheck"]))))
1287        return [self shortStatusString];
1288   
1289    return [self etaString: [self isSeeding] ? [self etaRatio] : [self eta]];
1290}
1291
1292- (NSString *) stateString
1293{
1294    switch (fStat->activity)
1295    {
1296        case TR_STATUS_STOPPED:
1297            return NSLocalizedString(@"Paused", "Torrent -> status string");
1298
1299        case TR_STATUS_CHECK:
1300            return [NSString localizedStringWithFormat: NSLocalizedString(@"Checking existing data (%.2f%%)",
1301                                    "Torrent -> status string"), 100.0 * [self checkingProgress]];
1302       
1303        case TR_STATUS_CHECK_WAIT:
1304            return [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1305
1306        case TR_STATUS_DOWNLOAD:
1307            return NSLocalizedString(@"Downloading", "Torrent -> status string");
1308
1309        case TR_STATUS_SEED:
1310            return NSLocalizedString(@"Seeding", "Torrent -> status string");
1311    }
1312}
1313
1314- (NSInteger) seeders
1315{
1316    return fStat->seeders;
1317}
1318
1319- (NSInteger) leechers
1320{
1321    return fStat->leechers;
1322}
1323
1324- (NSInteger) completedFromTracker
1325{
1326    return fStat->timesCompleted;
1327}
1328
1329- (NSInteger) totalPeersConnected
1330{
1331    return fStat->peersConnected;
1332}
1333
1334- (NSInteger) totalPeersTracker
1335{
1336    return fStat->peersFrom[TR_PEER_FROM_TRACKER];
1337}
1338
1339- (NSInteger) totalPeersIncoming
1340{
1341    return fStat->peersFrom[TR_PEER_FROM_INCOMING];
1342}
1343
1344- (NSInteger) totalPeersCache
1345{
1346    return fStat->peersFrom[TR_PEER_FROM_CACHE];
1347}
1348
1349- (NSInteger) totalPeersPex
1350{
1351    return fStat->peersFrom[TR_PEER_FROM_PEX];
1352}
1353
1354- (NSInteger) totalPeersKnown
1355{
1356    return fStat->peersKnown;
1357}
1358
1359- (NSInteger) peersSendingToUs
1360{
1361    return fStat->peersSendingToUs;
1362}
1363
1364- (NSInteger) peersGettingFromUs
1365{
1366    return fStat->peersGettingFromUs;
1367}
1368
1369- (CGFloat) downloadRate
1370{
1371    return fStat->pieceDownloadSpeed;
1372}
1373
1374- (CGFloat) uploadRate
1375{
1376    return fStat->pieceUploadSpeed;
1377}
1378
1379- (CGFloat) totalRate
1380{
1381    return [self downloadRate] + [self uploadRate];
1382}
1383
1384- (uint64_t) haveVerified
1385{
1386    return fStat->haveValid;
1387}
1388
1389- (uint64_t) haveTotal
1390{
1391    return [self haveVerified] + fStat->haveUnchecked;
1392}
1393
1394- (uint64_t) totalSizeSelected
1395{
1396    return fStat->sizeWhenDone;
1397}
1398
1399- (uint64_t) downloadedTotal
1400{
1401    return fStat->downloadedEver;
1402}
1403
1404- (uint64_t) uploadedTotal
1405{
1406    return fStat->uploadedEver;
1407}
1408
1409- (uint64_t) failedHash
1410{
1411    return fStat->corruptEver;
1412}
1413
1414- (CGFloat) swarmSpeed
1415{
1416    return fStat->swarmSpeed;
1417}
1418
1419- (NSInteger) orderValue
1420{
1421    return fOrderValue;
1422}
1423
1424- (void) setOrderValue: (NSInteger) orderValue
1425{
1426    fOrderValue = orderValue;
1427}
1428
1429- (NSInteger) groupValue
1430{
1431    return fGroupValue;
1432}
1433
1434- (void) setGroupValue: (NSInteger) goupValue
1435{
1436    fGroupValue = goupValue;
1437}
1438
1439- (NSInteger) groupOrderValue
1440{
1441    return [[GroupsController groups] rowValueForIndex: fGroupValue];
1442}
1443
1444- (void) checkGroupValueForRemoval: (NSNotification *) notification
1445{
1446    if (fGroupValue != -1 && [[[notification userInfo] objectForKey: @"Indexes"] containsIndex: fGroupValue])
1447        fGroupValue = -1;
1448}
1449
1450- (NSArray *) fileList
1451{
1452    return fFileList;
1453}
1454
1455- (NSInteger) fileCount
1456{
1457    return fInfo->fileCount;
1458}
1459
1460- (void) updateFileStat
1461{
1462    if (fFileStat)
1463        tr_torrentFilesFree(fFileStat, [self fileCount]);
1464   
1465    fFileStat = tr_torrentFiles(fHandle, NULL);
1466}
1467
1468- (CGFloat) fileProgress: (FileListNode *) node
1469{
1470    if ([self isComplete])
1471        return 1.0;
1472   
1473    if (!fFileStat)
1474        [self updateFileStat];
1475   
1476    NSIndexSet * indexSet = [node indexes];
1477   
1478    if ([indexSet count] == 1)
1479        return fFileStat[[indexSet firstIndex]].progress;
1480   
1481    uint64_t have = 0;
1482    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1483        have += fFileStat[index].bytesCompleted;
1484   
1485    NSAssert([node size], @"director in torrent file has size 0");
1486    return (CGFloat)have / [node size];
1487}
1488
1489- (BOOL) canChangeDownloadCheckForFile: (NSInteger) index
1490{
1491    if (!fFileStat)
1492        [self updateFileStat];
1493   
1494    return [self fileCount] > 1 && fFileStat[index].progress < 1.0;
1495}
1496
1497- (BOOL) canChangeDownloadCheckForFiles: (NSIndexSet *) indexSet
1498{
1499    if ([self fileCount] <= 1 || [self isComplete])
1500        return NO;
1501   
1502    if (!fFileStat)
1503        [self updateFileStat];
1504   
1505    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1506        if (fFileStat[index].progress < 1.0)
1507            return YES;
1508    return NO;
1509}
1510
1511- (NSInteger) checkForFiles: (NSIndexSet *) indexSet
1512{
1513    BOOL onState = NO, offState = NO;
1514    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1515    {
1516        if (tr_torrentGetFileDL(fHandle, index) || ![self canChangeDownloadCheckForFile: index])
1517            onState = YES;
1518        else
1519            offState = YES;
1520       
1521        if (onState && offState)
1522            return NSMixedState;
1523    }
1524    return onState ? NSOnState : NSOffState;
1525}
1526
1527- (void) setFileCheckState: (NSInteger) state forIndexes: (NSIndexSet *) indexSet
1528{
1529    NSUInteger count = [indexSet count];
1530    tr_file_index_t * files = malloc(count * sizeof(tr_file_index_t));
1531    for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++)
1532        files[i] = index;
1533   
1534    tr_torrentSetFileDLs(fHandle, files, count, state != NSOffState);
1535    free(files);
1536   
1537    [self update];
1538    [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFileCheckChange" object: self];
1539}
1540
1541- (void) setFilePriority: (NSInteger) priority forIndexes: (NSIndexSet *) indexSet
1542{
1543    const NSUInteger count = [indexSet count];
1544    tr_file_index_t * files = malloc(count * sizeof(tr_file_index_t));
1545    for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++)
1546        files[i] = index;
1547   
1548    tr_torrentSetFilePriorities(fHandle, files, count, priority);
1549    free(files);
1550}
1551
1552- (BOOL) hasFilePriority: (NSInteger) priority forIndexes: (NSIndexSet *) indexSet
1553{
1554    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1555        if (priority == tr_torrentGetFilePriority(fHandle, index) && [self canChangeDownloadCheckForFile: index])
1556            return YES;
1557    return NO;
1558}
1559
1560- (NSSet *) filePrioritiesForIndexes: (NSIndexSet *) indexSet
1561{
1562    BOOL low = NO, normal = NO, high = NO;
1563    NSMutableSet * priorities = [NSMutableSet setWithCapacity: 3];
1564   
1565    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1566    {
1567        if (![self canChangeDownloadCheckForFile: index])
1568            continue;
1569       
1570        NSInteger priority = tr_torrentGetFilePriority(fHandle, index);
1571        if (priority == TR_PRI_LOW)
1572        {
1573            if (low)
1574                continue;
1575            low = YES;
1576        }
1577        else if (priority == TR_PRI_HIGH)
1578        {
1579            if (high)
1580                continue;
1581            high = YES;
1582        }
1583        else
1584        {
1585            if (normal)
1586                continue;
1587            normal = YES;
1588        }
1589       
1590        [priorities addObject: [NSNumber numberWithInt: priority]];
1591        if (low && normal && high)
1592            break;
1593    }
1594    return priorities;
1595}
1596
1597- (NSDate *) dateAdded
1598{
1599    time_t date = fStat->addedDate;
1600    return [NSDate dateWithTimeIntervalSince1970: date];
1601}
1602
1603- (NSDate *) dateCompleted
1604{
1605    time_t date = fStat->doneDate;
1606    return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
1607}
1608
1609- (NSDate *) dateActivity
1610{
1611    time_t date = fStat->activityDate;
1612    return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
1613}
1614
1615- (NSDate *) dateActivityOrAdd
1616{
1617    NSDate * date = [self dateActivity];
1618    return date ? date : [self dateAdded];
1619}
1620
1621- (NSInteger) stalledMinutes
1622{
1623    time_t start = fStat->startDate;
1624    if (start == 0)
1625        return -1;
1626   
1627    NSDate * started = [NSDate dateWithTimeIntervalSince1970: start],
1628            * activity = [self dateActivity];
1629   
1630    NSDate * laterDate = activity ? [started laterDate: activity] : started;
1631    return -1 * [laterDate timeIntervalSinceNow] / 60;
1632}
1633
1634- (BOOL) isStalled
1635{
1636    return fStalled;
1637}
1638
1639- (NSInteger) stateSortKey
1640{
1641    if (![self isActive]) //paused
1642        return 0;
1643    else if ([self isSeeding]) //seeding
1644        return 1;
1645    else //downloading
1646        return 2;
1647}
1648
1649- (tr_torrent *) torrentStruct
1650{
1651    return fHandle;
1652}
1653
1654@end
1655
1656@implementation Torrent (Private)
1657
1658//if a hash is given, attempt to load that; otherwise, attempt to open file at path
1659- (id) initWithHash: (NSString *) hashString path: (NSString *) path torrentStruct: (tr_torrent *) torrentStruct lib: (tr_handle *) lib
1660        publicTorrent: (NSNumber *) publicTorrent
1661        downloadFolder: (NSString *) downloadFolder
1662        useIncompleteFolder: (NSNumber *) useIncompleteFolder incompleteFolder: (NSString *) incompleteFolder
1663        ratioSetting: (NSNumber *) ratioSetting ratioLimit: (NSNumber *) ratioLimit
1664        waitToStart: (NSNumber *) waitToStart
1665        orderValue: (NSNumber *) orderValue groupValue: (NSNumber *) groupValue addedTrackers: (NSNumber *) addedTrackers
1666{
1667    if (!(self = [super init]))
1668        return nil;
1669   
1670    fDefaults = [NSUserDefaults standardUserDefaults];
1671
1672    fPublicTorrent = path && (publicTorrent ? [publicTorrent boolValue] : ![fDefaults boolForKey: @"DeleteOriginalTorrent"]);
1673    if (fPublicTorrent)
1674        fPublicTorrentLocation = [path retain];
1675   
1676    fDownloadFolder = downloadFolder ? downloadFolder : [fDefaults stringForKey: @"DownloadFolder"];
1677    fDownloadFolder = [[fDownloadFolder stringByExpandingTildeInPath] retain];
1678   
1679    fUseIncompleteFolder = useIncompleteFolder ? [useIncompleteFolder boolValue]
1680                                : [fDefaults boolForKey: @"UseIncompleteDownloadFolder"];
1681    if (fUseIncompleteFolder)
1682    {
1683        fIncompleteFolder = incompleteFolder ? incompleteFolder : [fDefaults stringForKey: @"IncompleteDownloadFolder"];
1684        fIncompleteFolder = [[fIncompleteFolder stringByExpandingTildeInPath] retain];
1685    }
1686   
1687    if (torrentStruct)
1688    {
1689        fHandle = torrentStruct;
1690        fInfo = tr_torrentInfo(fHandle);
1691       
1692        NSString * currentDownloadFolder = [self shouldUseIncompleteFolderForName: [NSString stringWithUTF8String: fInfo->name]]
1693                                                ? fIncompleteFolder : fDownloadFolder;
1694        tr_torrentSetDownloadDir(fHandle, [currentDownloadFolder UTF8String]);
1695    }
1696    else
1697    {
1698        //set libtransmission settings for initialization
1699        tr_ctor * ctor = tr_ctorNew(lib);
1700        tr_ctorSetPaused(ctor, TR_FORCE, YES);
1701        tr_ctorSetPeerLimit(ctor, TR_FALLBACK, [fDefaults integerForKey: @"PeersTorrent"]);
1702       
1703        tr_info info;
1704        if (hashString)
1705        {
1706            tr_ctorSetMetainfoFromHash(ctor, [hashString UTF8String]);
1707            if (tr_torrentParse(lib, ctor, &info) == TR_OK)
1708            {
1709                NSString * currentDownloadFolder = [self shouldUseIncompleteFolderForName: [NSString stringWithUTF8String: info.name]]
1710                                                    ? fIncompleteFolder : fDownloadFolder;
1711                tr_ctorSetDownloadDir(ctor, TR_FORCE, [currentDownloadFolder UTF8String]);
1712               
1713                fHandle = tr_torrentNew(lib, ctor, NULL);
1714            }
1715            tr_metainfoFree(&info);
1716        }
1717        if (!fHandle && path)
1718        {
1719            tr_ctorSetMetainfoFromFile(ctor, [path UTF8String]);
1720            if (tr_torrentParse(lib, ctor, &info) == TR_OK)
1721            {
1722                NSString * currentDownloadFolder = [self shouldUseIncompleteFolderForName: [NSString stringWithUTF8String: info.name]]
1723                                                    ? fIncompleteFolder : fDownloadFolder;
1724                tr_ctorSetDownloadDir(ctor, TR_FORCE, [currentDownloadFolder UTF8String]);
1725               
1726                fHandle = tr_torrentNew(lib, ctor, NULL);
1727            }
1728            tr_metainfoFree(&info);
1729        }
1730       
1731        tr_ctorFree(ctor);
1732       
1733        if (!fHandle)
1734        {
1735            [self release];
1736            return nil;
1737        }
1738       
1739        fInfo = tr_torrentInfo(fHandle);
1740    }
1741   
1742    tr_torrentSetCompletenessCallback(fHandle, completenessChangeCallback, self);
1743   
1744    fNameString = [[NSString alloc] initWithUTF8String: fInfo->name];
1745    fHashString = [[NSString alloc] initWithUTF8String: fInfo->hashString];
1746       
1747    fRatioSetting = ratioSetting ? [ratioSetting intValue] : NSMixedState;
1748    fRatioLimit = ratioLimit ? [ratioLimit floatValue] : [fDefaults floatForKey: @"RatioLimit"];
1749    fFinishedSeeding = NO;
1750   
1751    fWaitToStart = waitToStart && [waitToStart boolValue];
1752    fResumeOnWake = NO;
1753   
1754    fOrderValue = orderValue ? [orderValue intValue] : tr_sessionCountTorrents(lib) - 1;
1755    fGroupValue = groupValue ? [groupValue intValue] : -1;
1756   
1757    fAddedTrackers = addedTrackers ? [addedTrackers boolValue] : NO;
1758   
1759    [self createFileList];
1760   
1761    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(checkGroupValueForRemoval:)
1762        name: @"GroupValueRemoved" object: nil];
1763   
1764    [self update];
1765   
1766    //mark incomplete files to be ignored by Time Machine
1767    [self setTimeMachineExclude: ![self allDownloaded] forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
1768   
1769    return self;
1770}
1771
1772- (void) createFileList
1773{
1774    if ([self isFolder])
1775    {
1776        NSInteger count = [self fileCount];
1777        NSMutableArray * fileList = [[NSMutableArray alloc] initWithCapacity: count];
1778       
1779        for (NSInteger i = 0; i < count; i++)
1780        {
1781            tr_file * file = &fInfo->files[i];
1782           
1783            NSMutableArray * pathComponents = [[[NSString stringWithUTF8String: file->name] pathComponents] mutableCopy];
1784            NSString * path = [pathComponents objectAtIndex: 0];
1785            NSString * name = [pathComponents objectAtIndex: 1];
1786            [pathComponents removeObjectsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, 2)]];
1787           
1788            if ([pathComponents count] > 0)
1789            {
1790                //determine if folder node already exists
1791                NSEnumerator * enumerator = [fileList objectEnumerator];
1792                FileListNode * node;
1793                while ((node = [enumerator nextObject]))
1794                    if ([[node name] isEqualToString: name] && [node isFolder])
1795                        break;
1796               
1797                if (!node)
1798                {
1799                    node = [[FileListNode alloc] initWithFolderName: name path: path];
1800                    [fileList addObject: node];
1801                    [node release];
1802                }
1803               
1804                [node insertIndex: i withSize: file->length];
1805                [self insertPath: pathComponents forParent: node fileSize: file->length index: i];
1806            }
1807            else
1808            {
1809                FileListNode * node = [[FileListNode alloc] initWithFileName: name path: path size: file->length index: i];
1810                [fileList addObject: node];
1811                [node release];
1812            }
1813           
1814            [pathComponents release];
1815        }
1816       
1817        fFileList = [[NSArray alloc] initWithArray: fileList];
1818        [fileList release];
1819    }
1820    else
1821    {
1822        FileListNode * node = [[FileListNode alloc] initWithFileName: [self name] path: @"" size: [self size] index: 0];
1823        fFileList = [[NSArray arrayWithObject: node] retain];
1824        [node release];
1825    }
1826}
1827
1828- (void) insertPath: (NSMutableArray *) components forParent: (FileListNode *) parent fileSize: (uint64_t) size index: (NSInteger) index
1829{
1830    NSString * name = [components objectAtIndex: 0];
1831    BOOL isFolder = [components count] > 1;
1832   
1833    FileListNode * node = nil;
1834    if (isFolder)
1835    {
1836        NSEnumerator * enumerator = [[parent children] objectEnumerator];
1837        while ((node = [enumerator nextObject]))
1838            if ([[node name] isEqualToString: name] && [node isFolder])
1839                break;
1840    }
1841   
1842    //create new folder or file if it doesn't already exist
1843    if (!node)
1844    {
1845        if (isFolder)
1846            node = [[FileListNode alloc] initWithFolderName: name path: [parent fullPath]];
1847        else
1848            node = [[FileListNode alloc] initWithFileName: name path: [parent fullPath] size: size index: index];
1849       
1850        [parent insertChild: node];
1851        [node release];
1852    }
1853   
1854    if (isFolder)
1855    {
1856        [node insertIndex: index withSize: size];
1857       
1858        [components removeObjectAtIndex: 0];
1859        [self insertPath: components forParent: node fileSize: size index: index];
1860    }
1861}
1862
1863- (BOOL) shouldUseIncompleteFolderForName: (NSString *) name
1864{
1865    return fUseIncompleteFolder &&
1866        ![[NSFileManager defaultManager] fileExistsAtPath: [fDownloadFolder stringByAppendingPathComponent: name]];
1867}
1868
1869- (void) updateDownloadFolder
1870{
1871    //remove old Time Machine location
1872    [self setTimeMachineExclude: NO forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
1873   
1874    NSString * folder = [self shouldUseIncompleteFolderForName: [self name]] ? fIncompleteFolder : fDownloadFolder;
1875    tr_torrentSetDownloadDir(fHandle, [folder UTF8String]);
1876   
1877    [self setTimeMachineExclude: ![self allDownloaded] forPath: [folder stringByAppendingPathComponent: [self name]]];
1878}
1879
1880//status has been retained
1881- (void) completenessChange: (NSNumber *) status
1882{
1883    fStat = tr_torrentStat(fHandle); //don't call update yet to avoid auto-stop
1884   
1885    BOOL canMove;
1886    switch ([status intValue])
1887    {
1888        case TR_CP_DONE:
1889        case TR_CP_COMPLETE:
1890            canMove = YES;
1891           
1892            //move file from incomplete folder to download folder
1893            if (fUseIncompleteFolder && ![[self downloadFolder] isEqualToString: fDownloadFolder]
1894                && (canMove = [self alertForMoveFolderAvailable]))
1895            {
1896                [self quickPause];
1897               
1898                if ([[NSFileManager defaultManager] movePath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]
1899                                        toPath: [fDownloadFolder stringByAppendingPathComponent: [self name]] handler: nil])
1900                    [self updateDownloadFolder];
1901                else
1902                    canMove = NO;
1903               
1904                [self endQuickPause];
1905            }
1906           
1907            if (!canMove)
1908            {
1909                fUseIncompleteFolder = NO;
1910               
1911                [fDownloadFolder release];
1912                fDownloadFolder = fIncompleteFolder;
1913                fIncompleteFolder = nil;
1914            }
1915           
1916            //allow to be backed up by Time Machine
1917            [self setTimeMachineExclude: NO forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
1918           
1919            [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedDownloading" object: self];
1920            break;
1921       
1922        case TR_CP_INCOMPLETE:
1923            //do not allow to be backed up by Time Machine
1924            [self setTimeMachineExclude: YES forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
1925           
1926            [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentRestartedDownloading" object: self];
1927            break;
1928    }
1929    [status release];
1930   
1931    [self update];
1932}
1933
1934- (void) quickPause
1935{
1936    if (fQuickPauseDict)
1937        return;
1938
1939    fQuickPauseDict = [[NSDictionary alloc] initWithObjectsAndKeys:
1940                    [NSNumber numberWithInt: [self speedMode: YES]], @"UploadSpeedMode",
1941                    [NSNumber numberWithInt: [self speedLimit: YES]], @"UploadSpeedLimit",
1942                    [NSNumber numberWithInt: [self speedMode: NO]], @"DownloadSpeedMode",
1943                    [NSNumber numberWithInt: [self speedLimit: NO]], @"DownloadSpeedLimit", nil];
1944   
1945    [self setSpeedMode: TR_SPEEDLIMIT_SINGLE upload: YES];
1946    [self setSpeedLimit: 0 upload: YES];
1947    [self setSpeedMode: TR_SPEEDLIMIT_SINGLE upload: NO];
1948    [self setSpeedLimit: 0 upload: NO];
1949}
1950
1951- (void) endQuickPause
1952{
1953    if (!fQuickPauseDict)
1954        return;
1955   
1956    [self setSpeedMode: [[fQuickPauseDict objectForKey: @"UploadSpeedMode"] intValue] upload: YES];
1957    [self setSpeedLimit: [[fQuickPauseDict objectForKey: @"UploadSpeedLimit"] intValue] upload: YES];
1958    [self setSpeedMode: [[fQuickPauseDict objectForKey: @"DownloadSpeedMode"] intValue] upload: NO];
1959    [self setSpeedLimit: [[fQuickPauseDict objectForKey: @"DownloadSpeedLimit"] intValue] upload: NO];
1960   
1961    [fQuickPauseDict release];
1962    fQuickPauseDict = nil;
1963}
1964
1965- (NSString *) etaString: (NSInteger) eta
1966{
1967    switch (eta)
1968    {
1969        case TR_ETA_NOT_AVAIL:
1970        case TR_ETA_UNKNOWN:
1971            return NSLocalizedString(@"remaining time unknown", "Torrent -> eta string");
1972        default:
1973            return [NSString stringWithFormat: NSLocalizedString(@"%@ remaining", "Torrent -> eta string"),
1974                        [NSString timeString: eta showSeconds: YES maxFields: 2]];
1975    }
1976}
1977
1978- (void) updateAllTrackers: (NSMutableArray *) trackers
1979{
1980    //get count
1981    NSInteger count = 0;
1982    NSEnumerator * enumerator = [trackers objectEnumerator];
1983    id object;
1984    while ((object = [enumerator nextObject]))
1985        if (![object isKindOfClass: [NSNumber class]])
1986            count++;
1987   
1988    //recreate the tracker structure
1989    tr_tracker_info * trackerStructs = tr_new(tr_tracker_info, count);
1990    NSInteger tier = 0, i = 0;
1991    enumerator = [trackers objectEnumerator];
1992    while ((object = [enumerator nextObject]))
1993    {
1994        if (![object isKindOfClass: [NSNumber class]])
1995        {
1996            trackerStructs[i].tier = tier;
1997            trackerStructs[i].announce = (char *)[object UTF8String];
1998            i++;
1999        }
2000        else
2001            tier++;
2002    }
2003   
2004    tr_torrentSetAnnounceList(fHandle, trackerStructs, count);
2005    tr_free(trackerStructs);
2006}
2007
2008- (void) trashFile: (NSString *) path
2009{
2010    //attempt to move to trash
2011    if (![[NSWorkspace sharedWorkspace] performFileOperation: NSWorkspaceRecycleOperation
2012        source: [path stringByDeletingLastPathComponent] destination: @""
2013        files: [NSArray arrayWithObject: [path lastPathComponent]] tag: nil])
2014    {
2015        //if cannot trash, just delete it (will work if it's on a remote volume)
2016        if ([NSApp isOnLeopardOrBetter])
2017        {
2018            NSError * error;
2019            if (![[NSFileManager defaultManager] removeItemAtPath: path error: &error])
2020                NSLog(@"Could not trash %@: %@", path, [error localizedDescription]);
2021        }
2022        else
2023        {
2024            if (![[NSFileManager defaultManager] removeFileAtPath: path handler: nil])
2025                NSLog(@"Could not trash %@", path);
2026        }
2027    }
2028}
2029
2030- (void) setTimeMachineExclude: (BOOL) exclude forPath: (NSString *) path
2031{
2032    if ([NSApp isOnLeopardOrBetter])
2033        CSBackupSetItemExcluded((CFURLRef)[NSURL fileURLWithPath: path], exclude, true);
2034}
2035
2036@end
Note: See TracBrowser for help on using the repository browser.