source: trunk/macosx/Torrent.m @ 7794

Last change on this file since 7794 was 7794, checked in by livings124, 13 years ago

determine queue order solely on order in the fTorrent array - this gets around potential problems when relaunching with less torrents

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