source: branches/2.0x/macosx/Torrent.m @ 10878

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

(2.0x) #3320 When a download completes at the same time seeding completes, still show a Growl notification and play a sound

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