2294 lines
65 KiB
Plaintext
2294 lines
65 KiB
Plaintext
/******************************************************************************
|
|
* Copyright (c) 2006-2012 Transmission authors and contributors
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
* DEALINGS IN THE SOFTWARE.
|
|
*****************************************************************************/
|
|
|
|
#include <libtransmission/transmission.h>
|
|
#include <libtransmission/error.h>
|
|
#include <libtransmission/log.h>
|
|
#include <libtransmission/utils.h> // tr_new()
|
|
|
|
#import "Torrent.h"
|
|
#import "GroupsController.h"
|
|
#import "FileListNode.h"
|
|
#import "NSApplicationAdditions.h"
|
|
#import "NSStringAdditions.h"
|
|
#import "TrackerNode.h"
|
|
|
|
#define ETA_IDLE_DISPLAY_SEC (2 * 60)
|
|
|
|
@interface Torrent (Private)
|
|
|
|
- (instancetype)initWithPath:(NSString*)path
|
|
hash:(NSString*)hashString
|
|
torrentStruct:(tr_torrent*)torrentStruct
|
|
magnetAddress:(NSString*)magnetAddress
|
|
lib:(tr_session*)lib
|
|
groupValue:(NSNumber*)groupValue
|
|
removeWhenFinishSeeding:(NSNumber*)removeWhenFinishSeeding
|
|
downloadFolder:(NSString*)downloadFolder
|
|
legacyIncompleteFolder:(NSString*)incompleteFolder;
|
|
|
|
- (void)createFileList;
|
|
- (void)insertPathForComponents:(NSArray*)components
|
|
withComponentIndex:(NSUInteger)componentIndex
|
|
forParent:(FileListNode*)parent
|
|
fileSize:(uint64_t)size
|
|
index:(NSInteger)index
|
|
flatList:(NSMutableArray*)flatFileList;
|
|
- (void)sortFileList:(NSMutableArray*)fileNodes;
|
|
|
|
- (void)startQueue;
|
|
- (void)completenessChange:(tr_completeness)status wasRunning:(BOOL)wasRunning;
|
|
- (void)ratioLimitHit;
|
|
- (void)idleLimitHit;
|
|
- (void)metadataRetrieved;
|
|
- (void)renameFinished:(BOOL)success
|
|
nodes:(NSArray*)nodes
|
|
completionHandler:(void (^)(BOOL))completionHandler
|
|
oldPath:(NSString*)oldPath
|
|
newName:(NSString*)newName;
|
|
|
|
@property(nonatomic, readonly) BOOL shouldShowEta;
|
|
@property(nonatomic, readonly) NSString* etaString;
|
|
|
|
- (void)setTimeMachineExclude:(BOOL)exclude;
|
|
|
|
@end
|
|
|
|
void startQueueCallback(tr_torrent* torrent, void* torrentData)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[(__bridge Torrent*)torrentData startQueue];
|
|
});
|
|
}
|
|
|
|
void completenessChangeCallback(tr_torrent* torrent, tr_completeness status, bool wasRunning, void* torrentData)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[(__bridge Torrent*)torrentData completenessChange:status wasRunning:wasRunning];
|
|
});
|
|
}
|
|
|
|
void ratioLimitHitCallback(tr_torrent* torrent, void* torrentData)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[(__bridge Torrent*)torrentData ratioLimitHit];
|
|
});
|
|
}
|
|
|
|
void idleLimitHitCallback(tr_torrent* torrent, void* torrentData)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[(__bridge Torrent*)torrentData idleLimitHit];
|
|
});
|
|
}
|
|
|
|
void metadataCallback(tr_torrent* torrent, void* torrentData)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[(__bridge Torrent*)torrentData metadataRetrieved];
|
|
});
|
|
}
|
|
|
|
void renameCallback(tr_torrent* torrent, char const* oldPathCharString, char const* newNameCharString, int error, void* contextInfo)
|
|
{
|
|
@autoreleasepool
|
|
{
|
|
NSString* oldPath = @(oldPathCharString);
|
|
NSString* newName = @(newNameCharString);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSDictionary* contextDict = (__bridge_transfer NSDictionary*)contextInfo;
|
|
Torrent* torrentObject = contextDict[@"Torrent"];
|
|
[torrentObject renameFinished:error == 0 nodes:contextDict[@"Nodes"]
|
|
completionHandler:contextDict[@"CompletionHandler"]
|
|
oldPath:oldPath
|
|
newName:newName];
|
|
});
|
|
}
|
|
}
|
|
|
|
bool trashDataFile(char const* filename, tr_error** error)
|
|
{
|
|
if (filename == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@autoreleasepool
|
|
{
|
|
NSError* localError;
|
|
if (![Torrent trashFile:@(filename) error:&localError])
|
|
{
|
|
tr_error_set_literal(error, localError.code, localError.description.UTF8String);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@implementation Torrent
|
|
{
|
|
tr_torrent* fHandle;
|
|
tr_info const* fInfo;
|
|
tr_stat const* fStat;
|
|
|
|
NSUserDefaults* fDefaults;
|
|
|
|
NSImage* fIcon;
|
|
|
|
NSString* fHashString;
|
|
|
|
tr_file_stat* fFileStat;
|
|
NSArray* fFileList;
|
|
NSArray* fFlatFileList;
|
|
|
|
NSIndexSet* fPreviousFinishedIndexes;
|
|
NSDate* fPreviousFinishedIndexesDate;
|
|
|
|
NSInteger fGroupValue;
|
|
TorrentDeterminationType fGroupValueDetermination;
|
|
|
|
TorrentDeterminationType fDownloadFolderDetermination;
|
|
|
|
BOOL fResumeOnWake;
|
|
|
|
BOOL fTimeMachineExcludeInitialized;
|
|
}
|
|
|
|
- (instancetype)initWithPath:(NSString*)path
|
|
location:(NSString*)location
|
|
deleteTorrentFile:(BOOL)torrentDelete
|
|
lib:(tr_session*)lib
|
|
{
|
|
self = [self initWithPath:path hash:nil torrentStruct:NULL magnetAddress:nil lib:lib groupValue:nil
|
|
removeWhenFinishSeeding:nil
|
|
downloadFolder:location
|
|
legacyIncompleteFolder:nil];
|
|
|
|
if (self)
|
|
{
|
|
if (torrentDelete && ![self.torrentLocation isEqualToString:path])
|
|
{
|
|
[Torrent trashFile:path error:nil];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithTorrentStruct:(tr_torrent*)torrentStruct location:(NSString*)location lib:(tr_session*)lib
|
|
{
|
|
self = [self initWithPath:nil hash:nil torrentStruct:torrentStruct magnetAddress:nil lib:lib groupValue:nil
|
|
removeWhenFinishSeeding:nil
|
|
downloadFolder:location
|
|
legacyIncompleteFolder:nil];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithMagnetAddress:(NSString*)address location:(NSString*)location lib:(tr_session*)lib
|
|
{
|
|
self = [self initWithPath:nil hash:nil torrentStruct:nil magnetAddress:address lib:lib groupValue:nil
|
|
removeWhenFinishSeeding:nil
|
|
downloadFolder:location
|
|
legacyIncompleteFolder:nil];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithHistory:(NSDictionary*)history lib:(tr_session*)lib forcePause:(BOOL)pause
|
|
{
|
|
self = [self initWithPath:history[@"InternalTorrentPath"] hash:history[@"TorrentHash"] torrentStruct:NULL magnetAddress:nil
|
|
lib:lib
|
|
groupValue:history[@"GroupValue"]
|
|
removeWhenFinishSeeding:history[@"RemoveWhenFinishSeeding"]
|
|
downloadFolder:history[@"DownloadFolder"] //upgrading from versions < 1.80
|
|
legacyIncompleteFolder:[history[@"UseIncompleteFolder"] boolValue] //upgrading from versions < 1.80
|
|
?
|
|
history[@"IncompleteFolder"] :
|
|
nil];
|
|
|
|
if (self)
|
|
{
|
|
//start transfer
|
|
NSNumber* active;
|
|
if (!pause && (active = history[@"Active"]) && active.boolValue)
|
|
{
|
|
fStat = tr_torrentStat(fHandle);
|
|
[self startTransferNoQueue];
|
|
}
|
|
|
|
//upgrading from versions < 1.30: get old added, activity, and done dates
|
|
NSDate* date;
|
|
if ((date = history[@"Date"]))
|
|
{
|
|
tr_torrentSetAddedDate(fHandle, date.timeIntervalSince1970);
|
|
}
|
|
if ((date = history[@"DateActivity"]))
|
|
{
|
|
tr_torrentSetActivityDate(fHandle, date.timeIntervalSince1970);
|
|
}
|
|
if ((date = history[@"DateCompleted"]))
|
|
{
|
|
tr_torrentSetDoneDate(fHandle, date.timeIntervalSince1970);
|
|
}
|
|
|
|
//upgrading from versions < 1.60: get old stop ratio settings
|
|
NSNumber* ratioSetting;
|
|
if ((ratioSetting = history[@"RatioSetting"]))
|
|
{
|
|
switch (ratioSetting.intValue)
|
|
{
|
|
case NSOnState:
|
|
self.ratioSetting = TR_RATIOLIMIT_SINGLE;
|
|
break;
|
|
case NSOffState:
|
|
self.ratioSetting = TR_RATIOLIMIT_UNLIMITED;
|
|
break;
|
|
case NSMixedState:
|
|
self.ratioSetting = TR_RATIOLIMIT_GLOBAL;
|
|
break;
|
|
}
|
|
}
|
|
NSNumber* ratioLimit;
|
|
if ((ratioLimit = history[@"RatioLimit"]))
|
|
{
|
|
self.ratioLimit = ratioLimit.floatValue;
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSDictionary*)history
|
|
{
|
|
return @{
|
|
@"InternalTorrentPath" : self.torrentLocation,
|
|
@"TorrentHash" : self.hashString,
|
|
@"Active" : @(self.active),
|
|
@"WaitToStart" : @(self.waitingToStart),
|
|
@"GroupValue" : @(fGroupValue),
|
|
@"RemoveWhenFinishSeeding" : @(_removeWhenFinishSeeding)
|
|
};
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[NSNotificationCenter.defaultCenter removeObserver:self];
|
|
|
|
if (fFileStat)
|
|
{
|
|
tr_torrentFilesFree(fFileStat, self.fileCount);
|
|
}
|
|
}
|
|
|
|
- (NSString*)description
|
|
{
|
|
return [@"Torrent: " stringByAppendingString:self.name];
|
|
}
|
|
|
|
- (id)copyWithZone:(NSZone*)zone
|
|
{
|
|
return self;
|
|
}
|
|
|
|
- (void)closeRemoveTorrent:(BOOL)trashFiles
|
|
{
|
|
//allow the file to be indexed by Time Machine
|
|
[self setTimeMachineExclude:NO];
|
|
|
|
tr_torrentRemove(fHandle, trashFiles, trashDataFile);
|
|
}
|
|
|
|
- (void)changeDownloadFolderBeforeUsing:(NSString*)folder determinationType:(TorrentDeterminationType)determinationType
|
|
{
|
|
//if data existed in original download location, unexclude it before changing the location
|
|
[self setTimeMachineExclude:NO];
|
|
|
|
tr_torrentSetDownloadDir(fHandle, folder.UTF8String);
|
|
|
|
fDownloadFolderDetermination = determinationType;
|
|
}
|
|
|
|
- (NSString*)currentDirectory
|
|
{
|
|
return @(tr_torrentGetCurrentDir(fHandle));
|
|
}
|
|
|
|
- (void)getAvailability:(int8_t*)tab size:(NSInteger)size
|
|
{
|
|
tr_torrentAvailability(fHandle, tab, size);
|
|
}
|
|
|
|
- (void)getAmountFinished:(float*)tab size:(NSInteger)size
|
|
{
|
|
tr_torrentAmountFinished(fHandle, tab, size);
|
|
}
|
|
|
|
- (NSIndexSet*)previousFinishedPieces
|
|
{
|
|
//if the torrent hasn't been seen in a bit, and therefore hasn't been refreshed, return nil
|
|
if (fPreviousFinishedIndexesDate && fPreviousFinishedIndexesDate.timeIntervalSinceNow > -2.0)
|
|
{
|
|
return fPreviousFinishedIndexes;
|
|
}
|
|
else
|
|
{
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (void)setPreviousFinishedPieces:(NSIndexSet*)indexes
|
|
{
|
|
fPreviousFinishedIndexes = indexes;
|
|
|
|
fPreviousFinishedIndexesDate = indexes != nil ? [[NSDate alloc] init] : nil;
|
|
}
|
|
|
|
- (void)update
|
|
{
|
|
//get previous stalled value before update
|
|
BOOL const wasStalled = fStat != NULL && self.stalled;
|
|
|
|
fStat = tr_torrentStat(fHandle);
|
|
|
|
//make sure the "active" filter is updated when stalled-ness changes
|
|
if (wasStalled != self.stalled)
|
|
{
|
|
//posting asynchronously with coalescing to prevent stack overflow on lots of torrents changing state at the same time
|
|
[NSNotificationQueue.defaultQueue enqueueNotification:[NSNotification notificationWithName:@"UpdateQueue" object:self]
|
|
postingStyle:NSPostASAP
|
|
coalesceMask:NSNotificationCoalescingOnName
|
|
forModes:nil];
|
|
}
|
|
|
|
//when the torrent is first loaded, update the time machine exclusion
|
|
if (!fTimeMachineExcludeInitialized)
|
|
{
|
|
[self updateTimeMachineExclude];
|
|
}
|
|
}
|
|
|
|
- (void)startTransferIgnoringQueue:(BOOL)ignoreQueue
|
|
{
|
|
if ([self alertForRemainingDiskSpace])
|
|
{
|
|
ignoreQueue ? tr_torrentStartNow(fHandle) : tr_torrentStart(fHandle);
|
|
[self update];
|
|
|
|
//capture, specifically, stop-seeding settings changing to unlimited
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"UpdateOptions" object:nil];
|
|
}
|
|
}
|
|
|
|
- (void)startTransferNoQueue
|
|
{
|
|
[self startTransferIgnoringQueue:YES];
|
|
}
|
|
|
|
- (void)startTransfer
|
|
{
|
|
[self startTransferIgnoringQueue:NO];
|
|
}
|
|
|
|
- (void)stopTransfer
|
|
{
|
|
tr_torrentStop(fHandle);
|
|
[self update];
|
|
}
|
|
|
|
- (void)sleep
|
|
{
|
|
if ((fResumeOnWake = self.active))
|
|
{
|
|
tr_torrentStop(fHandle);
|
|
}
|
|
}
|
|
|
|
- (void)wakeUp
|
|
{
|
|
if (fResumeOnWake)
|
|
{
|
|
tr_logAddNamedInfo(fInfo->name, "restarting because of wakeUp");
|
|
tr_torrentStart(fHandle);
|
|
}
|
|
}
|
|
|
|
- (NSUInteger)queuePosition
|
|
{
|
|
return fStat->queuePosition;
|
|
}
|
|
|
|
- (void)setQueuePosition:(NSUInteger)index
|
|
{
|
|
tr_torrentSetQueuePosition(fHandle, index);
|
|
}
|
|
|
|
- (void)manualAnnounce
|
|
{
|
|
tr_torrentManualUpdate(fHandle);
|
|
}
|
|
|
|
- (BOOL)canManualAnnounce
|
|
{
|
|
return tr_torrentCanManualUpdate(fHandle);
|
|
}
|
|
|
|
- (void)resetCache
|
|
{
|
|
tr_torrentVerify(fHandle, NULL, NULL);
|
|
[self update];
|
|
}
|
|
|
|
- (BOOL)isMagnet
|
|
{
|
|
return !tr_torrentHasMetadata(fHandle);
|
|
}
|
|
|
|
- (NSString*)magnetLink
|
|
{
|
|
return @(tr_torrentGetMagnetLink(fHandle));
|
|
}
|
|
|
|
- (CGFloat)ratio
|
|
{
|
|
return fStat->ratio;
|
|
}
|
|
|
|
- (tr_ratiolimit)ratioSetting
|
|
{
|
|
return tr_torrentGetRatioMode(fHandle);
|
|
}
|
|
|
|
- (void)setRatioSetting:(tr_ratiolimit)setting
|
|
{
|
|
tr_torrentSetRatioMode(fHandle, setting);
|
|
}
|
|
|
|
- (CGFloat)ratioLimit
|
|
{
|
|
return tr_torrentGetRatioLimit(fHandle);
|
|
}
|
|
|
|
- (void)setRatioLimit:(CGFloat)limit
|
|
{
|
|
NSParameterAssert(limit >= 0);
|
|
|
|
tr_torrentSetRatioLimit(fHandle, limit);
|
|
}
|
|
|
|
- (CGFloat)progressStopRatio
|
|
{
|
|
return fStat->seedRatioPercentDone;
|
|
}
|
|
|
|
- (tr_idlelimit)idleSetting
|
|
{
|
|
return tr_torrentGetIdleMode(fHandle);
|
|
}
|
|
|
|
- (void)setIdleSetting:(tr_idlelimit)setting
|
|
{
|
|
tr_torrentSetIdleMode(fHandle, setting);
|
|
}
|
|
|
|
- (NSUInteger)idleLimitMinutes
|
|
{
|
|
return tr_torrentGetIdleLimit(fHandle);
|
|
}
|
|
|
|
- (void)setIdleLimitMinutes:(NSUInteger)limit
|
|
{
|
|
NSParameterAssert(limit > 0);
|
|
|
|
tr_torrentSetIdleLimit(fHandle, limit);
|
|
}
|
|
|
|
- (BOOL)usesSpeedLimit:(BOOL)upload
|
|
{
|
|
return tr_torrentUsesSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN);
|
|
}
|
|
|
|
- (void)setUseSpeedLimit:(BOOL)use upload:(BOOL)upload
|
|
{
|
|
tr_torrentUseSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN, use);
|
|
}
|
|
|
|
- (NSInteger)speedLimit:(BOOL)upload
|
|
{
|
|
return tr_torrentGetSpeedLimit_KBps(fHandle, upload ? TR_UP : TR_DOWN);
|
|
}
|
|
|
|
- (void)setSpeedLimit:(NSInteger)limit upload:(BOOL)upload
|
|
{
|
|
tr_torrentSetSpeedLimit_KBps(fHandle, upload ? TR_UP : TR_DOWN, limit);
|
|
}
|
|
|
|
- (BOOL)usesGlobalSpeedLimit
|
|
{
|
|
return tr_torrentUsesSessionLimits(fHandle);
|
|
}
|
|
|
|
- (void)setUseGlobalSpeedLimit:(BOOL)use
|
|
{
|
|
tr_torrentUseSessionLimits(fHandle, use);
|
|
}
|
|
|
|
- (void)setMaxPeerConnect:(uint16_t)count
|
|
{
|
|
NSParameterAssert(count > 0);
|
|
|
|
tr_torrentSetPeerLimit(fHandle, count);
|
|
}
|
|
|
|
- (uint16_t)maxPeerConnect
|
|
{
|
|
return tr_torrentGetPeerLimit(fHandle);
|
|
}
|
|
- (BOOL)waitingToStart
|
|
{
|
|
return fStat->activity == TR_STATUS_DOWNLOAD_WAIT || fStat->activity == TR_STATUS_SEED_WAIT;
|
|
}
|
|
|
|
- (tr_priority_t)priority
|
|
{
|
|
return tr_torrentGetPriority(fHandle);
|
|
}
|
|
|
|
- (void)setPriority:(tr_priority_t)priority
|
|
{
|
|
return tr_torrentSetPriority(fHandle, priority);
|
|
}
|
|
|
|
+ (BOOL)trashFile:(NSString*)path error:(NSError**)error
|
|
{
|
|
//attempt to move to trash
|
|
if (![NSWorkspace.sharedWorkspace performFileOperation:NSWorkspaceRecycleOperation source:path.stringByDeletingLastPathComponent
|
|
destination:@""
|
|
files:@[ path.lastPathComponent ]
|
|
tag:nil])
|
|
{
|
|
//if cannot trash, just delete it (will work if it's on a remote volume)
|
|
NSError* localError;
|
|
if (![NSFileManager.defaultManager removeItemAtPath:path error:&localError])
|
|
{
|
|
NSLog(@"old Could not trash %@: %@", path, localError.localizedDescription);
|
|
if (error != nil)
|
|
{
|
|
*error = localError;
|
|
}
|
|
return NO;
|
|
}
|
|
else
|
|
{
|
|
NSLog(@"old removed %@", path);
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)moveTorrentDataFileTo:(NSString*)folder
|
|
{
|
|
NSString* oldFolder = self.currentDirectory;
|
|
if ([oldFolder isEqualToString:folder])
|
|
{
|
|
return;
|
|
}
|
|
|
|
//check if moving inside itself
|
|
NSArray *oldComponents = oldFolder.pathComponents, *newComponents = folder.pathComponents;
|
|
NSUInteger const oldCount = oldComponents.count;
|
|
|
|
if (oldCount < newComponents.count && [newComponents[oldCount] isEqualToString:self.name] && [folder hasPrefix:oldFolder])
|
|
{
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
alert.messageText = NSLocalizedString(@"A folder cannot be moved to inside itself.", "Move inside itself alert -> title");
|
|
alert.informativeText = [NSString
|
|
stringWithFormat:NSLocalizedString(@"The move operation of \"%@\" cannot be done.", "Move inside itself alert -> message"),
|
|
self.name];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Move inside itself alert -> button")];
|
|
|
|
[alert runModal];
|
|
|
|
return;
|
|
}
|
|
|
|
volatile int status;
|
|
tr_torrentSetLocation(fHandle, folder.UTF8String, YES, NULL, &status);
|
|
|
|
while (status == TR_LOC_MOVING) //block while moving (for now)
|
|
{
|
|
[NSThread sleepForTimeInterval:0.05];
|
|
}
|
|
|
|
if (status == TR_LOC_DONE)
|
|
{
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"UpdateStats" object:nil];
|
|
}
|
|
else
|
|
{
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
alert.messageText = NSLocalizedString(@"There was an error moving the data file.", "Move error alert -> title");
|
|
alert.informativeText = [NSString
|
|
stringWithFormat:NSLocalizedString(@"The move operation of \"%@\" cannot be done.", "Move error alert -> message"), self.name];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Move error alert -> button")];
|
|
|
|
[alert runModal];
|
|
}
|
|
|
|
[self updateTimeMachineExclude];
|
|
}
|
|
|
|
- (void)copyTorrentFileTo:(NSString*)path
|
|
{
|
|
[NSFileManager.defaultManager copyItemAtPath:self.torrentLocation toPath:path error:NULL];
|
|
}
|
|
|
|
- (BOOL)alertForRemainingDiskSpace
|
|
{
|
|
if (self.allDownloaded || ![fDefaults boolForKey:@"WarningRemainingSpace"])
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
NSString* downloadFolder = self.currentDirectory;
|
|
NSDictionary* systemAttributes;
|
|
if ((systemAttributes = [NSFileManager.defaultManager attributesOfFileSystemForPath:downloadFolder error:NULL]))
|
|
{
|
|
uint64_t const remainingSpace = ((NSNumber*)systemAttributes[NSFileSystemFreeSize]).unsignedLongLongValue;
|
|
|
|
//if the remaining space is greater than the size left, then there is enough space regardless of preallocation
|
|
if (remainingSpace < self.sizeLeft && remainingSpace < tr_torrentGetBytesLeftToAllocate(fHandle))
|
|
{
|
|
NSString* volumeName = [NSFileManager.defaultManager componentsToDisplayForPath:downloadFolder][0];
|
|
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
alert.messageText = [NSString
|
|
stringWithFormat:NSLocalizedString(@"Not enough remaining disk space to download \"%@\" completely.", "Torrent disk space alert -> title"),
|
|
self.name];
|
|
alert.informativeText = [NSString stringWithFormat:NSLocalizedString(
|
|
@"The transfer will be paused."
|
|
" Clear up space on %@ or deselect files in the torrent inspector to continue.",
|
|
"Torrent disk space alert -> message"),
|
|
volumeName];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Torrent disk space alert -> button")];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"Download Anyway", "Torrent disk space alert -> button")];
|
|
|
|
alert.showsSuppressionButton = YES;
|
|
alert.suppressionButton.title = NSLocalizedString(@"Do not check disk space again", "Torrent disk space alert -> button");
|
|
|
|
NSInteger const result = [alert runModal];
|
|
if (alert.suppressionButton.state == NSOnState)
|
|
{
|
|
[fDefaults setBool:NO forKey:@"WarningRemainingSpace"];
|
|
}
|
|
|
|
return result != NSAlertFirstButtonReturn;
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (NSImage*)icon
|
|
{
|
|
if (self.magnet)
|
|
{
|
|
return [NSImage imageNamed:@"Magnet"];
|
|
}
|
|
|
|
if (!fIcon)
|
|
{
|
|
fIcon = self.folder ? [NSImage imageNamed:NSImageNameFolder] : [NSWorkspace.sharedWorkspace iconForFileType:self.name.pathExtension];
|
|
}
|
|
return fIcon;
|
|
}
|
|
|
|
- (NSString*)name
|
|
{
|
|
return fInfo->name != NULL ? @(fInfo->name) : fHashString;
|
|
}
|
|
|
|
- (BOOL)isFolder
|
|
{
|
|
return fInfo->isFolder;
|
|
}
|
|
|
|
- (uint64_t)size
|
|
{
|
|
return fInfo->totalSize;
|
|
}
|
|
|
|
- (uint64_t)sizeLeft
|
|
{
|
|
return fStat->leftUntilDone;
|
|
}
|
|
|
|
- (NSMutableArray*)allTrackerStats
|
|
{
|
|
int count;
|
|
tr_tracker_stat* stats = tr_torrentTrackers(fHandle, &count);
|
|
|
|
NSMutableArray* trackers = [NSMutableArray arrayWithCapacity:(count > 0 ? count + (stats[count - 1].tier + 1) : 0)];
|
|
|
|
int prevTier = -1;
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
if (stats[i].tier != prevTier)
|
|
{
|
|
[trackers addObject:@{ @"Tier" : @(stats[i].tier + 1), @"Name" : self.name }];
|
|
prevTier = stats[i].tier;
|
|
}
|
|
|
|
TrackerNode* tracker = [[TrackerNode alloc] initWithTrackerStat:&stats[i] torrent:self];
|
|
[trackers addObject:tracker];
|
|
}
|
|
|
|
tr_torrentTrackersFree(stats, count);
|
|
return trackers;
|
|
}
|
|
|
|
- (NSArray*)allTrackersFlat
|
|
{
|
|
NSMutableArray* allTrackers = [NSMutableArray arrayWithCapacity:fInfo->trackerCount];
|
|
|
|
for (NSInteger i = 0; i < fInfo->trackerCount; i++)
|
|
{
|
|
[allTrackers addObject:@(fInfo->trackers[i].announce)];
|
|
}
|
|
|
|
return allTrackers;
|
|
}
|
|
|
|
- (BOOL)addTrackerToNewTier:(NSString*)tracker
|
|
{
|
|
tracker = [tracker stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
|
|
|
if ([tracker rangeOfString:@"://"].location == NSNotFound)
|
|
{
|
|
tracker = [@"http://" stringByAppendingString:tracker];
|
|
}
|
|
|
|
//recreate the tracker structure
|
|
int const oldTrackerCount = fInfo->trackerCount;
|
|
tr_tracker_info* trackerStructs = tr_new(tr_tracker_info, oldTrackerCount + 1);
|
|
for (int i = 0; i < oldTrackerCount; ++i)
|
|
{
|
|
trackerStructs[i] = fInfo->trackers[i];
|
|
}
|
|
|
|
trackerStructs[oldTrackerCount].announce = (char*)tracker.UTF8String;
|
|
trackerStructs[oldTrackerCount].tier = trackerStructs[oldTrackerCount - 1].tier + 1;
|
|
trackerStructs[oldTrackerCount].id = oldTrackerCount;
|
|
|
|
BOOL const success = tr_torrentSetAnnounceList(fHandle, trackerStructs, oldTrackerCount + 1);
|
|
tr_free(trackerStructs);
|
|
|
|
return success;
|
|
}
|
|
|
|
- (void)removeTrackers:(NSSet*)trackers
|
|
{
|
|
//recreate the tracker structure
|
|
tr_tracker_info* trackerStructs = tr_new(tr_tracker_info, fInfo->trackerCount);
|
|
|
|
NSUInteger newCount = 0;
|
|
for (NSUInteger i = 0; i < fInfo->trackerCount; i++)
|
|
{
|
|
if (![trackers containsObject:@(fInfo->trackers[i].announce)])
|
|
{
|
|
trackerStructs[newCount++] = fInfo->trackers[i];
|
|
}
|
|
}
|
|
|
|
BOOL const success = tr_torrentSetAnnounceList(fHandle, trackerStructs, newCount);
|
|
NSAssert(success, @"Removing tracker addresses failed");
|
|
|
|
tr_free(trackerStructs);
|
|
}
|
|
|
|
- (NSString*)comment
|
|
{
|
|
return fInfo->comment ? @(fInfo->comment) : @"";
|
|
}
|
|
|
|
- (NSString*)creator
|
|
{
|
|
return fInfo->creator ? @(fInfo->creator) : @"";
|
|
}
|
|
|
|
- (NSDate*)dateCreated
|
|
{
|
|
NSInteger date = fInfo->dateCreated;
|
|
return date > 0 ? [NSDate dateWithTimeIntervalSince1970:date] : nil;
|
|
}
|
|
|
|
- (NSInteger)pieceSize
|
|
{
|
|
return fInfo->pieceSize;
|
|
}
|
|
|
|
- (NSInteger)pieceCount
|
|
{
|
|
return fInfo->pieceCount;
|
|
}
|
|
|
|
- (NSString*)hashString
|
|
{
|
|
return fHashString;
|
|
}
|
|
|
|
- (BOOL)privateTorrent
|
|
{
|
|
return fInfo->isPrivate;
|
|
}
|
|
|
|
- (NSString*)torrentLocation
|
|
{
|
|
return fInfo->torrent ? @(fInfo->torrent) : @"";
|
|
}
|
|
|
|
- (NSString*)dataLocation
|
|
{
|
|
if (self.magnet)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
if (self.folder)
|
|
{
|
|
NSString* dataLocation = [self.currentDirectory stringByAppendingPathComponent:self.name];
|
|
|
|
if (![NSFileManager.defaultManager fileExistsAtPath:dataLocation])
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
return dataLocation;
|
|
}
|
|
else
|
|
{
|
|
char* location = tr_torrentFindFile(fHandle, 0);
|
|
if (location == NULL)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
NSString* dataLocation = @(location);
|
|
free(location);
|
|
|
|
return dataLocation;
|
|
}
|
|
}
|
|
|
|
- (NSString*)fileLocation:(FileListNode*)node
|
|
{
|
|
if (node.isFolder)
|
|
{
|
|
NSString* basePath = [node.path stringByAppendingPathComponent:node.name];
|
|
NSString* dataLocation = [self.currentDirectory stringByAppendingPathComponent:basePath];
|
|
|
|
if (![NSFileManager.defaultManager fileExistsAtPath:dataLocation])
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
return dataLocation;
|
|
}
|
|
else
|
|
{
|
|
char* location = tr_torrentFindFile(fHandle, node.indexes.firstIndex);
|
|
if (location == NULL)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
NSString* dataLocation = @(location);
|
|
free(location);
|
|
|
|
return dataLocation;
|
|
}
|
|
}
|
|
|
|
- (void)renameTorrent:(NSString*)newName completionHandler:(void (^)(BOOL didRename))completionHandler
|
|
{
|
|
NSParameterAssert(newName != nil);
|
|
NSParameterAssert(![newName isEqualToString:@""]);
|
|
|
|
NSDictionary* contextInfo = @{ @"Torrent" : self, @"CompletionHandler" : [completionHandler copy] };
|
|
|
|
tr_torrentRenamePath(fHandle, fInfo->name, newName.UTF8String, renameCallback, (__bridge_retained void*)(contextInfo));
|
|
}
|
|
|
|
- (void)renameFileNode:(FileListNode*)node
|
|
withName:(NSString*)newName
|
|
completionHandler:(void (^)(BOOL didRename))completionHandler
|
|
{
|
|
NSParameterAssert(node.torrent == self);
|
|
NSParameterAssert(newName != nil);
|
|
NSParameterAssert(![newName isEqualToString:@""]);
|
|
|
|
NSDictionary* contextInfo = @{ @"Torrent" : self, @"Nodes" : @[ node ], @"CompletionHandler" : [completionHandler copy] };
|
|
|
|
NSString* oldPath = [node.path stringByAppendingPathComponent:node.name];
|
|
tr_torrentRenamePath(fHandle, oldPath.UTF8String, newName.UTF8String, renameCallback, (__bridge_retained void*)(contextInfo));
|
|
}
|
|
|
|
- (CGFloat)progress
|
|
{
|
|
return fStat->percentComplete;
|
|
}
|
|
|
|
- (CGFloat)progressDone
|
|
{
|
|
return fStat->percentDone;
|
|
}
|
|
|
|
- (CGFloat)progressLeft
|
|
{
|
|
if (self.size == 0) //magnet links
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
return (CGFloat)self.sizeLeft / self.size;
|
|
}
|
|
|
|
- (CGFloat)checkingProgress
|
|
{
|
|
return fStat->recheckProgress;
|
|
}
|
|
|
|
- (CGFloat)availableDesired
|
|
{
|
|
return (CGFloat)fStat->desiredAvailable / self.sizeLeft;
|
|
}
|
|
|
|
- (BOOL)isActive
|
|
{
|
|
return fStat->activity != TR_STATUS_STOPPED && fStat->activity != TR_STATUS_DOWNLOAD_WAIT && fStat->activity != TR_STATUS_SEED_WAIT;
|
|
}
|
|
|
|
- (BOOL)isSeeding
|
|
{
|
|
return fStat->activity == TR_STATUS_SEED;
|
|
}
|
|
|
|
- (BOOL)isChecking
|
|
{
|
|
return fStat->activity == TR_STATUS_CHECK || fStat->activity == TR_STATUS_CHECK_WAIT;
|
|
}
|
|
|
|
- (BOOL)isCheckingWaiting
|
|
{
|
|
return fStat->activity == TR_STATUS_CHECK_WAIT;
|
|
}
|
|
|
|
- (BOOL)allDownloaded
|
|
{
|
|
return self.sizeLeft == 0 && !self.magnet;
|
|
}
|
|
|
|
- (BOOL)isComplete
|
|
{
|
|
return self.progress >= 1.0;
|
|
}
|
|
|
|
- (BOOL)isFinishedSeeding
|
|
{
|
|
return fStat->finished;
|
|
}
|
|
|
|
- (BOOL)isError
|
|
{
|
|
return fStat->error == TR_STAT_LOCAL_ERROR;
|
|
}
|
|
|
|
- (BOOL)isAnyErrorOrWarning
|
|
{
|
|
return fStat->error != TR_STAT_OK;
|
|
}
|
|
|
|
- (NSString*)errorMessage
|
|
{
|
|
if (!self.anyErrorOrWarning)
|
|
{
|
|
return @"";
|
|
}
|
|
|
|
NSString* error;
|
|
if (!(error = @(fStat->errorString)) && !(error = [NSString stringWithCString:fStat->errorString encoding:NSISOLatin1StringEncoding]))
|
|
{
|
|
error = [NSString stringWithFormat:@"(%@)", NSLocalizedString(@"unreadable error", "Torrent -> error string unreadable")];
|
|
}
|
|
|
|
//libtransmission uses "Set Location", Mac client uses "Move data file to..." - very hacky!
|
|
error = [error stringByReplacingOccurrencesOfString:@"Set Location" withString:[@"Move Data File To" stringByAppendingEllipsis]];
|
|
|
|
return error;
|
|
}
|
|
|
|
- (NSArray*)peers
|
|
{
|
|
int totalPeers;
|
|
tr_peer_stat* peers = tr_torrentPeers(fHandle, &totalPeers);
|
|
|
|
NSMutableArray* peerDicts = [NSMutableArray arrayWithCapacity:totalPeers];
|
|
|
|
for (int i = 0; i < totalPeers; i++)
|
|
{
|
|
tr_peer_stat* peer = &peers[i];
|
|
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:12];
|
|
|
|
dict[@"Name"] = self.name;
|
|
dict[@"From"] = @(peer->from);
|
|
dict[@"IP"] = @(peer->addr);
|
|
dict[@"Port"] = @(peer->port);
|
|
dict[@"Progress"] = @(peer->progress);
|
|
dict[@"Seed"] = @(peer->isSeed);
|
|
dict[@"Encryption"] = @(peer->isEncrypted);
|
|
dict[@"uTP"] = @(peer->isUTP);
|
|
dict[@"Client"] = @(peer->client);
|
|
dict[@"Flags"] = @(peer->flagStr);
|
|
|
|
if (peer->isUploadingTo)
|
|
{
|
|
dict[@"UL To Rate"] = @(peer->rateToPeer_KBps);
|
|
}
|
|
if (peer->isDownloadingFrom)
|
|
{
|
|
dict[@"DL From Rate"] = @(peer->rateToClient_KBps);
|
|
}
|
|
|
|
[peerDicts addObject:dict];
|
|
}
|
|
|
|
tr_torrentPeersFree(peers, totalPeers);
|
|
|
|
return peerDicts;
|
|
}
|
|
|
|
- (NSUInteger)webSeedCount
|
|
{
|
|
return fInfo->webseedCount;
|
|
}
|
|
|
|
- (NSArray*)webSeeds
|
|
{
|
|
NSMutableArray* webSeeds = [NSMutableArray arrayWithCapacity:fInfo->webseedCount];
|
|
|
|
double* dlSpeeds = tr_torrentWebSpeeds_KBps(fHandle);
|
|
|
|
for (NSInteger i = 0; i < fInfo->webseedCount; i++)
|
|
{
|
|
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:3];
|
|
|
|
dict[@"Name"] = self.name;
|
|
dict[@"Address"] = @(fInfo->webseeds[i]);
|
|
|
|
if (dlSpeeds[i] != -1.0)
|
|
{
|
|
dict[@"DL From Rate"] = @(dlSpeeds[i]);
|
|
}
|
|
|
|
[webSeeds addObject:dict];
|
|
}
|
|
|
|
tr_free(dlSpeeds);
|
|
|
|
return webSeeds;
|
|
}
|
|
|
|
- (NSString*)progressString
|
|
{
|
|
if (self.magnet)
|
|
{
|
|
NSString* progressString = fStat->metadataPercentComplete > 0.0 ?
|
|
[NSString stringWithFormat:NSLocalizedString(@"%@ of torrent metadata retrieved", "Torrent -> progress string"),
|
|
[NSString percentString:fStat->metadataPercentComplete longDecimals:YES]] :
|
|
NSLocalizedString(@"torrent metadata needed", "Torrent -> progress string");
|
|
|
|
return [NSString stringWithFormat:@"%@ - %@", NSLocalizedString(@"Magnetized transfer", "Torrent -> progress string"), progressString];
|
|
}
|
|
|
|
NSString* string;
|
|
|
|
if (!self.allDownloaded)
|
|
{
|
|
CGFloat progress;
|
|
if (self.folder && [fDefaults boolForKey:@"DisplayStatusProgressSelected"])
|
|
{
|
|
string = [NSString stringForFilePartialSize:self.haveTotal fullSize:self.totalSizeSelected];
|
|
progress = self.progressDone;
|
|
}
|
|
else
|
|
{
|
|
string = [NSString stringForFilePartialSize:self.haveTotal fullSize:self.size];
|
|
progress = self.progress;
|
|
}
|
|
|
|
string = [string stringByAppendingFormat:@" (%@)", [NSString percentString:progress longDecimals:YES]];
|
|
}
|
|
else
|
|
{
|
|
NSString* downloadString;
|
|
if (!self.complete) //only multifile possible
|
|
{
|
|
if ([fDefaults boolForKey:@"DisplayStatusProgressSelected"])
|
|
{
|
|
downloadString = [NSString stringWithFormat:NSLocalizedString(@"%@ selected", "Torrent -> progress string"),
|
|
[NSString stringForFileSize:self.haveTotal]];
|
|
}
|
|
else
|
|
{
|
|
downloadString = [NSString stringForFilePartialSize:self.haveTotal fullSize:self.size];
|
|
downloadString = [downloadString stringByAppendingFormat:@" (%@)", [NSString percentString:self.progress longDecimals:YES]];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
downloadString = [NSString stringForFileSize:self.size];
|
|
}
|
|
|
|
NSString* uploadString = [NSString stringWithFormat:NSLocalizedString(@"uploaded %@ (Ratio: %@)", "Torrent -> progress string"),
|
|
[NSString stringForFileSize:self.uploadedTotal],
|
|
[NSString stringForRatio:self.ratio]];
|
|
|
|
string = [downloadString stringByAppendingFormat:@", %@", uploadString];
|
|
}
|
|
|
|
//add time when downloading or seed limit set
|
|
if (self.shouldShowEta)
|
|
{
|
|
string = [string stringByAppendingFormat:@" - %@", self.etaString];
|
|
}
|
|
|
|
return string;
|
|
}
|
|
|
|
- (NSString*)statusString
|
|
{
|
|
NSString* string;
|
|
|
|
if (self.anyErrorOrWarning)
|
|
{
|
|
switch (fStat->error)
|
|
{
|
|
case TR_STAT_LOCAL_ERROR:
|
|
string = NSLocalizedString(@"Error", "Torrent -> status string");
|
|
break;
|
|
case TR_STAT_TRACKER_ERROR:
|
|
string = NSLocalizedString(@"Tracker returned error", "Torrent -> status string");
|
|
break;
|
|
case TR_STAT_TRACKER_WARNING:
|
|
string = NSLocalizedString(@"Tracker returned warning", "Torrent -> status string");
|
|
break;
|
|
default:
|
|
NSAssert(NO, @"unknown error state");
|
|
}
|
|
|
|
NSString* errorString = self.errorMessage;
|
|
if (errorString && ![errorString isEqualToString:@""])
|
|
{
|
|
string = [string stringByAppendingFormat:@": %@", errorString];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (fStat->activity)
|
|
{
|
|
case TR_STATUS_STOPPED:
|
|
if (self.finishedSeeding)
|
|
{
|
|
string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
|
|
}
|
|
else
|
|
{
|
|
string = NSLocalizedString(@"Paused", "Torrent -> status string");
|
|
}
|
|
break;
|
|
|
|
case TR_STATUS_DOWNLOAD_WAIT:
|
|
string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis];
|
|
break;
|
|
|
|
case TR_STATUS_SEED_WAIT:
|
|
string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
|
|
break;
|
|
|
|
case TR_STATUS_CHECK_WAIT:
|
|
string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
|
|
break;
|
|
|
|
case TR_STATUS_CHECK:
|
|
string = [NSString stringWithFormat:@"%@ (%@)",
|
|
NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
|
|
[NSString percentString:self.checkingProgress longDecimals:YES]];
|
|
break;
|
|
|
|
case TR_STATUS_DOWNLOAD:
|
|
if (self.totalPeersConnected != 1)
|
|
{
|
|
string = [NSString stringWithFormat:NSLocalizedString(@"Downloading from %d of %d peers", "Torrent -> status string"),
|
|
self.peersSendingToUs,
|
|
self.totalPeersConnected];
|
|
}
|
|
else
|
|
{
|
|
string = [NSString stringWithFormat:NSLocalizedString(@"Downloading from %d of 1 peer", "Torrent -> status string"),
|
|
self.peersSendingToUs];
|
|
}
|
|
|
|
if (NSInteger const webSeedCount = fStat->webseedsSendingToUs; webSeedCount > 0)
|
|
{
|
|
NSString* webSeedString;
|
|
if (webSeedCount == 1)
|
|
{
|
|
webSeedString = NSLocalizedString(@"web seed", "Torrent -> status string");
|
|
}
|
|
else
|
|
{
|
|
webSeedString = [NSString stringWithFormat:NSLocalizedString(@"%d web seeds", "Torrent -> status string"), webSeedCount];
|
|
}
|
|
|
|
string = [string stringByAppendingFormat:@" + %@", webSeedString];
|
|
}
|
|
|
|
break;
|
|
|
|
case TR_STATUS_SEED:
|
|
if (self.totalPeersConnected != 1)
|
|
{
|
|
string = [NSString stringWithFormat:NSLocalizedString(@"Seeding to %d of %d peers", "Torrent -> status string"),
|
|
self.peersGettingFromUs,
|
|
self.totalPeersConnected];
|
|
}
|
|
else
|
|
{
|
|
string = [NSString stringWithFormat:NSLocalizedString(@"Seeding to %d of 1 peer", "Torrent -> status string"),
|
|
self.peersGettingFromUs];
|
|
}
|
|
}
|
|
|
|
if (self.stalled)
|
|
{
|
|
string = [NSLocalizedString(@"Stalled", "Torrent -> status string") stringByAppendingFormat:@", %@", string];
|
|
}
|
|
}
|
|
|
|
//append even if error
|
|
if (self.active && !self.checking)
|
|
{
|
|
if (fStat->activity == TR_STATUS_DOWNLOAD)
|
|
{
|
|
string = [string stringByAppendingFormat:@" - %@: %@, %@: %@",
|
|
NSLocalizedString(@"DL", "Torrent -> status string"),
|
|
[NSString stringForSpeed:self.downloadRate],
|
|
NSLocalizedString(@"UL", "Torrent -> status string"),
|
|
[NSString stringForSpeed:self.uploadRate]];
|
|
}
|
|
else
|
|
{
|
|
string = [string stringByAppendingFormat:@" - %@: %@",
|
|
NSLocalizedString(@"UL", "Torrent -> status string"),
|
|
[NSString stringForSpeed:self.uploadRate]];
|
|
}
|
|
}
|
|
|
|
return string;
|
|
}
|
|
|
|
- (NSString*)shortStatusString
|
|
{
|
|
NSString* string;
|
|
|
|
switch (fStat->activity)
|
|
{
|
|
case TR_STATUS_STOPPED:
|
|
if (self.finishedSeeding)
|
|
{
|
|
string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
|
|
}
|
|
else
|
|
{
|
|
string = NSLocalizedString(@"Paused", "Torrent -> status string");
|
|
}
|
|
break;
|
|
|
|
case TR_STATUS_DOWNLOAD_WAIT:
|
|
string = [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis];
|
|
break;
|
|
|
|
case TR_STATUS_SEED_WAIT:
|
|
string = [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
|
|
break;
|
|
|
|
case TR_STATUS_CHECK_WAIT:
|
|
string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
|
|
break;
|
|
|
|
case TR_STATUS_CHECK:
|
|
string = [NSString stringWithFormat:@"%@ (%@)",
|
|
NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
|
|
[NSString percentString:self.checkingProgress longDecimals:YES]];
|
|
break;
|
|
|
|
case TR_STATUS_DOWNLOAD:
|
|
string = [NSString stringWithFormat:@"%@: %@, %@: %@",
|
|
NSLocalizedString(@"DL", "Torrent -> status string"),
|
|
[NSString stringForSpeed:self.downloadRate],
|
|
NSLocalizedString(@"UL", "Torrent -> status string"),
|
|
[NSString stringForSpeed:self.uploadRate]];
|
|
break;
|
|
|
|
case TR_STATUS_SEED:
|
|
string = [NSString stringWithFormat:@"%@: %@, %@: %@",
|
|
NSLocalizedString(@"Ratio", "Torrent -> status string"),
|
|
[NSString stringForRatio:self.ratio],
|
|
NSLocalizedString(@"UL", "Torrent -> status string"),
|
|
[NSString stringForSpeed:self.uploadRate]];
|
|
}
|
|
|
|
return string;
|
|
}
|
|
|
|
- (NSString*)remainingTimeString
|
|
{
|
|
if (self.shouldShowEta)
|
|
{
|
|
return self.etaString;
|
|
}
|
|
else
|
|
{
|
|
return self.shortStatusString;
|
|
}
|
|
}
|
|
|
|
- (NSString*)stateString
|
|
{
|
|
switch (fStat->activity)
|
|
{
|
|
case TR_STATUS_STOPPED:
|
|
case TR_STATUS_DOWNLOAD_WAIT:
|
|
case TR_STATUS_SEED_WAIT:
|
|
{
|
|
NSString* string = NSLocalizedString(@"Paused", "Torrent -> status string");
|
|
|
|
NSString* extra = nil;
|
|
if (self.waitingToStart)
|
|
{
|
|
extra = fStat->activity == TR_STATUS_DOWNLOAD_WAIT ?
|
|
NSLocalizedString(@"Waiting to download", "Torrent -> status string") :
|
|
NSLocalizedString(@"Waiting to seed", "Torrent -> status string");
|
|
}
|
|
else if (self.finishedSeeding)
|
|
{
|
|
extra = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
|
|
}
|
|
|
|
return extra ? [string stringByAppendingFormat:@" (%@)", extra] : string;
|
|
}
|
|
|
|
case TR_STATUS_CHECK_WAIT:
|
|
return [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
|
|
|
|
case TR_STATUS_CHECK:
|
|
return [NSString stringWithFormat:@"%@ (%@)",
|
|
NSLocalizedString(@"Checking existing data", "Torrent -> status string"),
|
|
[NSString percentString:self.checkingProgress longDecimals:YES]];
|
|
|
|
case TR_STATUS_DOWNLOAD:
|
|
return NSLocalizedString(@"Downloading", "Torrent -> status string");
|
|
|
|
case TR_STATUS_SEED:
|
|
return NSLocalizedString(@"Seeding", "Torrent -> status string");
|
|
}
|
|
}
|
|
|
|
- (NSInteger)totalPeersConnected
|
|
{
|
|
return fStat->peersConnected;
|
|
}
|
|
|
|
- (NSInteger)totalPeersTracker
|
|
{
|
|
return fStat->peersFrom[TR_PEER_FROM_TRACKER];
|
|
}
|
|
|
|
- (NSInteger)totalPeersIncoming
|
|
{
|
|
return fStat->peersFrom[TR_PEER_FROM_INCOMING];
|
|
}
|
|
|
|
- (NSInteger)totalPeersCache
|
|
{
|
|
return fStat->peersFrom[TR_PEER_FROM_RESUME];
|
|
}
|
|
|
|
- (NSInteger)totalPeersPex
|
|
{
|
|
return fStat->peersFrom[TR_PEER_FROM_PEX];
|
|
}
|
|
|
|
- (NSInteger)totalPeersDHT
|
|
{
|
|
return fStat->peersFrom[TR_PEER_FROM_DHT];
|
|
}
|
|
|
|
- (NSInteger)totalPeersLocal
|
|
{
|
|
return fStat->peersFrom[TR_PEER_FROM_LPD];
|
|
}
|
|
|
|
- (NSInteger)totalPeersLTEP
|
|
{
|
|
return fStat->peersFrom[TR_PEER_FROM_LTEP];
|
|
}
|
|
|
|
- (NSInteger)peersSendingToUs
|
|
{
|
|
return fStat->peersSendingToUs;
|
|
}
|
|
|
|
- (NSInteger)peersGettingFromUs
|
|
{
|
|
return fStat->peersGettingFromUs;
|
|
}
|
|
|
|
- (CGFloat)downloadRate
|
|
{
|
|
return fStat->pieceDownloadSpeed_KBps;
|
|
}
|
|
|
|
- (CGFloat)uploadRate
|
|
{
|
|
return fStat->pieceUploadSpeed_KBps;
|
|
}
|
|
|
|
- (CGFloat)totalRate
|
|
{
|
|
return self.downloadRate + self.uploadRate;
|
|
}
|
|
|
|
- (uint64_t)haveVerified
|
|
{
|
|
return fStat->haveValid;
|
|
}
|
|
|
|
- (uint64_t)haveTotal
|
|
{
|
|
return self.haveVerified + fStat->haveUnchecked;
|
|
}
|
|
|
|
- (uint64_t)totalSizeSelected
|
|
{
|
|
return fStat->sizeWhenDone;
|
|
}
|
|
|
|
- (uint64_t)downloadedTotal
|
|
{
|
|
return fStat->downloadedEver;
|
|
}
|
|
|
|
- (uint64_t)uploadedTotal
|
|
{
|
|
return fStat->uploadedEver;
|
|
}
|
|
|
|
- (uint64_t)failedHash
|
|
{
|
|
return fStat->corruptEver;
|
|
}
|
|
|
|
- (NSInteger)groupValue
|
|
{
|
|
return fGroupValue;
|
|
}
|
|
|
|
- (void)setGroupValue:(NSInteger)groupValue determinationType:(TorrentDeterminationType)determinationType
|
|
{
|
|
if (groupValue != fGroupValue)
|
|
{
|
|
fGroupValue = groupValue;
|
|
[NSNotificationCenter.defaultCenter postNotificationName:kTorrentDidChangeGroupNotification object:self];
|
|
}
|
|
fGroupValueDetermination = determinationType;
|
|
}
|
|
|
|
- (NSInteger)groupOrderValue
|
|
{
|
|
return [GroupsController.groups rowValueForIndex:fGroupValue];
|
|
}
|
|
|
|
- (void)checkGroupValueForRemoval:(NSNotification*)notification
|
|
{
|
|
if (fGroupValue != -1 && [notification.userInfo[@"Index"] integerValue] == fGroupValue)
|
|
{
|
|
fGroupValue = -1;
|
|
}
|
|
}
|
|
|
|
- (NSArray*)fileList
|
|
{
|
|
return fFileList;
|
|
}
|
|
|
|
- (NSArray*)flatFileList
|
|
{
|
|
return fFlatFileList;
|
|
}
|
|
|
|
- (NSInteger)fileCount
|
|
{
|
|
return fInfo->fileCount;
|
|
}
|
|
|
|
- (void)updateFileStat
|
|
{
|
|
if (fFileStat)
|
|
{
|
|
tr_torrentFilesFree(fFileStat, self.fileCount);
|
|
}
|
|
|
|
fFileStat = tr_torrentFiles(fHandle, NULL);
|
|
}
|
|
|
|
- (CGFloat)fileProgress:(FileListNode*)node
|
|
{
|
|
if (self.fileCount == 1 || self.complete)
|
|
{
|
|
return self.progress;
|
|
}
|
|
|
|
if (!fFileStat)
|
|
{
|
|
[self updateFileStat];
|
|
}
|
|
|
|
// #5501
|
|
if (node.size == 0)
|
|
{
|
|
return 1.0;
|
|
}
|
|
|
|
NSIndexSet* indexSet = node.indexes;
|
|
|
|
if (indexSet.count == 1)
|
|
{
|
|
return fFileStat[indexSet.firstIndex].progress;
|
|
}
|
|
|
|
uint64_t have = 0;
|
|
for (NSInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
|
|
{
|
|
have += fFileStat[index].bytesCompleted;
|
|
}
|
|
|
|
return (CGFloat)have / node.size;
|
|
}
|
|
|
|
- (BOOL)canChangeDownloadCheckForFile:(NSUInteger)index
|
|
{
|
|
NSAssert2((NSInteger)index < self.fileCount, @"Index %ld is greater than file count %ld", index, self.fileCount);
|
|
|
|
return [self canChangeDownloadCheckForFiles:[NSIndexSet indexSetWithIndex:index]];
|
|
}
|
|
|
|
- (BOOL)canChangeDownloadCheckForFiles:(NSIndexSet*)indexSet
|
|
{
|
|
if (self.fileCount == 1 || self.complete)
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
if (!fFileStat)
|
|
{
|
|
[self updateFileStat];
|
|
}
|
|
|
|
__block BOOL canChange = NO;
|
|
[indexSet enumerateIndexesWithOptions:NSEnumerationConcurrent usingBlock:^(NSUInteger index, BOOL* stop) {
|
|
if (fFileStat[index].progress < 1.0)
|
|
{
|
|
canChange = YES;
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
return canChange;
|
|
}
|
|
|
|
- (NSInteger)checkForFiles:(NSIndexSet*)indexSet
|
|
{
|
|
BOOL onState = NO, offState = NO;
|
|
for (NSUInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
|
|
{
|
|
if (!fInfo->files[index].dnd || ![self canChangeDownloadCheckForFile:index])
|
|
{
|
|
onState = YES;
|
|
}
|
|
else
|
|
{
|
|
offState = YES;
|
|
}
|
|
|
|
if (onState && offState)
|
|
{
|
|
return NSMixedState;
|
|
}
|
|
}
|
|
return onState ? NSOnState : NSOffState;
|
|
}
|
|
|
|
- (void)setFileCheckState:(NSInteger)state forIndexes:(NSIndexSet*)indexSet
|
|
{
|
|
NSUInteger count = indexSet.count;
|
|
tr_file_index_t* files = static_cast<tr_file_index_t*>(malloc(count * sizeof(tr_file_index_t)));
|
|
for (NSUInteger index = indexSet.firstIndex, i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index], i++)
|
|
{
|
|
files[i] = index;
|
|
}
|
|
|
|
tr_torrentSetFileDLs(fHandle, files, count, state != NSOffState);
|
|
free(files);
|
|
|
|
[self update];
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"TorrentFileCheckChange" object:self];
|
|
}
|
|
|
|
- (void)setFilePriority:(tr_priority_t)priority forIndexes:(NSIndexSet*)indexSet
|
|
{
|
|
NSUInteger const count = indexSet.count;
|
|
tr_file_index_t* files = static_cast<tr_file_index_t*>(tr_malloc(count * sizeof(tr_file_index_t)));
|
|
for (NSUInteger index = indexSet.firstIndex, i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index], i++)
|
|
{
|
|
files[i] = index;
|
|
}
|
|
|
|
tr_torrentSetFilePriorities(fHandle, files, count, priority);
|
|
tr_free(files);
|
|
}
|
|
|
|
- (BOOL)hasFilePriority:(tr_priority_t)priority forIndexes:(NSIndexSet*)indexSet
|
|
{
|
|
for (NSUInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
|
|
{
|
|
if (priority == fInfo->files[index].priority && [self canChangeDownloadCheckForFile:index])
|
|
{
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (NSSet*)filePrioritiesForIndexes:(NSIndexSet*)indexSet
|
|
{
|
|
BOOL low = NO, normal = NO, high = NO;
|
|
NSMutableSet* priorities = [NSMutableSet setWithCapacity:MIN(indexSet.count, 3u)];
|
|
|
|
for (NSUInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
|
|
{
|
|
if (![self canChangeDownloadCheckForFile:index])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
tr_priority_t const priority = fInfo->files[index].priority;
|
|
switch (priority)
|
|
{
|
|
case TR_PRI_LOW:
|
|
if (low)
|
|
{
|
|
continue;
|
|
}
|
|
low = YES;
|
|
break;
|
|
case TR_PRI_NORMAL:
|
|
if (normal)
|
|
{
|
|
continue;
|
|
}
|
|
normal = YES;
|
|
break;
|
|
case TR_PRI_HIGH:
|
|
if (high)
|
|
{
|
|
continue;
|
|
}
|
|
high = YES;
|
|
break;
|
|
default:
|
|
NSAssert2(NO, @"Unknown priority %d for file index %ld", priority, index);
|
|
}
|
|
|
|
[priorities addObject:@(priority)];
|
|
if (low && normal && high)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return priorities;
|
|
}
|
|
|
|
- (NSDate*)dateAdded
|
|
{
|
|
time_t const date = fStat->addedDate;
|
|
return [NSDate dateWithTimeIntervalSince1970:date];
|
|
}
|
|
|
|
- (NSDate*)dateCompleted
|
|
{
|
|
time_t const date = fStat->doneDate;
|
|
return date != 0 ? [NSDate dateWithTimeIntervalSince1970:date] : nil;
|
|
}
|
|
|
|
- (NSDate*)dateActivity
|
|
{
|
|
time_t const date = fStat->activityDate;
|
|
return date != 0 ? [NSDate dateWithTimeIntervalSince1970:date] : nil;
|
|
}
|
|
|
|
- (NSDate*)dateActivityOrAdd
|
|
{
|
|
NSDate* date = self.dateActivity;
|
|
return date ? date : self.dateAdded;
|
|
}
|
|
|
|
- (NSInteger)secondsDownloading
|
|
{
|
|
return fStat->secondsDownloading;
|
|
}
|
|
|
|
- (NSInteger)secondsSeeding
|
|
{
|
|
return fStat->secondsSeeding;
|
|
}
|
|
|
|
- (NSInteger)stalledMinutes
|
|
{
|
|
if (fStat->idleSecs == -1)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
return fStat->idleSecs / 60;
|
|
}
|
|
|
|
- (BOOL)isStalled
|
|
{
|
|
return fStat->isStalled;
|
|
}
|
|
|
|
- (void)updateTimeMachineExclude
|
|
{
|
|
[self setTimeMachineExclude:!self.allDownloaded];
|
|
}
|
|
|
|
- (NSInteger)stateSortKey
|
|
{
|
|
if (!self.active) //paused
|
|
{
|
|
if (self.waitingToStart)
|
|
{
|
|
return 1;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
else if (self.seeding) //seeding
|
|
{
|
|
return 10;
|
|
}
|
|
else //downloading
|
|
{
|
|
return 20;
|
|
}
|
|
}
|
|
|
|
- (NSString*)trackerSortKey
|
|
{
|
|
int count;
|
|
tr_tracker_stat* stats = tr_torrentTrackers(fHandle, &count);
|
|
|
|
NSString* best = nil;
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
NSString* tracker = @(stats[i].host);
|
|
if (!best || [tracker localizedCaseInsensitiveCompare:best] == NSOrderedAscending)
|
|
{
|
|
best = tracker;
|
|
}
|
|
}
|
|
|
|
tr_torrentTrackersFree(stats, count);
|
|
return best;
|
|
}
|
|
|
|
- (tr_torrent*)torrentStruct
|
|
{
|
|
return fHandle;
|
|
}
|
|
|
|
- (NSURL*)previewItemURL
|
|
{
|
|
NSString* location = self.dataLocation;
|
|
return location ? [NSURL fileURLWithPath:location] : nil;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation Torrent (Private)
|
|
|
|
- (instancetype)initWithPath:(NSString*)path
|
|
hash:(NSString*)hashString
|
|
torrentStruct:(tr_torrent*)torrentStruct
|
|
magnetAddress:(NSString*)magnetAddress
|
|
lib:(tr_session*)lib
|
|
groupValue:(NSNumber*)groupValue
|
|
removeWhenFinishSeeding:(NSNumber*)removeWhenFinishSeeding
|
|
downloadFolder:(NSString*)downloadFolder
|
|
legacyIncompleteFolder:(NSString*)incompleteFolder
|
|
{
|
|
if (!(self = [super init]))
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
fDefaults = NSUserDefaults.standardUserDefaults;
|
|
|
|
if (torrentStruct)
|
|
{
|
|
fHandle = torrentStruct;
|
|
}
|
|
else
|
|
{
|
|
//set libtransmission settings for initialization
|
|
tr_ctor* ctor = tr_ctorNew(lib);
|
|
|
|
tr_ctorSetPaused(ctor, TR_FORCE, YES);
|
|
if (downloadFolder)
|
|
{
|
|
tr_ctorSetDownloadDir(ctor, TR_FORCE, downloadFolder.UTF8String);
|
|
}
|
|
if (incompleteFolder)
|
|
{
|
|
tr_ctorSetIncompleteDir(ctor, incompleteFolder.UTF8String);
|
|
}
|
|
|
|
tr_parse_result result = TR_PARSE_ERR;
|
|
if (path)
|
|
{
|
|
result = static_cast<tr_parse_result>(tr_ctorSetMetainfoFromFile(ctor, path.UTF8String));
|
|
}
|
|
|
|
if (result != TR_PARSE_OK && magnetAddress)
|
|
{
|
|
result = static_cast<tr_parse_result>(tr_ctorSetMetainfoFromMagnetLink(ctor, magnetAddress.UTF8String));
|
|
}
|
|
|
|
//backup - shouldn't be needed after upgrade to 1.70
|
|
if (result != TR_PARSE_OK && hashString)
|
|
{
|
|
result = static_cast<tr_parse_result>(tr_ctorSetMetainfoFromHash(ctor, hashString.UTF8String));
|
|
}
|
|
|
|
if (result == TR_PARSE_OK)
|
|
{
|
|
fHandle = tr_torrentNew(ctor, NULL, NULL);
|
|
}
|
|
|
|
tr_ctorFree(ctor);
|
|
|
|
if (!fHandle)
|
|
{
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
fInfo = tr_torrentInfo(fHandle);
|
|
|
|
tr_torrentSetQueueStartCallback(fHandle, startQueueCallback, (__bridge void*)(self));
|
|
tr_torrentSetCompletenessCallback(fHandle, completenessChangeCallback, (__bridge void*)(self));
|
|
tr_torrentSetRatioLimitHitCallback(fHandle, ratioLimitHitCallback, (__bridge void*)(self));
|
|
tr_torrentSetIdleLimitHitCallback(fHandle, idleLimitHitCallback, (__bridge void*)(self));
|
|
tr_torrentSetMetadataCallback(fHandle, metadataCallback, (__bridge void*)(self));
|
|
|
|
fHashString = @(fInfo->hashString);
|
|
|
|
fResumeOnWake = NO;
|
|
|
|
//don't do after this point - it messes with auto-group functionality
|
|
if (!self.magnet)
|
|
{
|
|
[self createFileList];
|
|
}
|
|
|
|
fDownloadFolderDetermination = TorrentDeterminationAutomatic;
|
|
|
|
if (groupValue)
|
|
{
|
|
fGroupValueDetermination = TorrentDeterminationUserSpecified;
|
|
fGroupValue = groupValue.intValue;
|
|
}
|
|
else
|
|
{
|
|
fGroupValueDetermination = TorrentDeterminationAutomatic;
|
|
fGroupValue = [GroupsController.groups groupIndexForTorrent:self];
|
|
}
|
|
|
|
_removeWhenFinishSeeding = removeWhenFinishSeeding ? removeWhenFinishSeeding.boolValue :
|
|
[fDefaults boolForKey:@"RemoveWhenFinishSeeding"];
|
|
|
|
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(checkGroupValueForRemoval:)
|
|
name:@"GroupValueRemoved"
|
|
object:nil];
|
|
|
|
fTimeMachineExcludeInitialized = NO;
|
|
[self update];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)createFileList
|
|
{
|
|
NSAssert(!self.magnet, @"Cannot create a file list until the torrent is demagnetized");
|
|
|
|
if (self.folder)
|
|
{
|
|
NSInteger const count = self.fileCount;
|
|
NSMutableArray* flatFileList = [NSMutableArray arrayWithCapacity:count];
|
|
|
|
FileListNode* tempNode = nil;
|
|
|
|
for (NSInteger i = 0; i < count; i++)
|
|
{
|
|
tr_file* file = &fInfo->files[i];
|
|
|
|
NSString* fullPath = @(file->name);
|
|
NSArray* pathComponents = fullPath.pathComponents;
|
|
|
|
if (!tempNode)
|
|
{
|
|
tempNode = [[FileListNode alloc] initWithFolderName:pathComponents[0] path:@"" torrent:self];
|
|
}
|
|
|
|
[self insertPathForComponents:pathComponents
|
|
withComponentIndex:1
|
|
forParent:tempNode
|
|
fileSize:file->length
|
|
index:i
|
|
flatList:flatFileList];
|
|
}
|
|
|
|
[self sortFileList:tempNode.children];
|
|
[self sortFileList:flatFileList];
|
|
|
|
fFileList = [[NSArray alloc] initWithArray:tempNode.children];
|
|
fFlatFileList = [[NSArray alloc] initWithArray:flatFileList];
|
|
}
|
|
else
|
|
{
|
|
FileListNode* node = [[FileListNode alloc] initWithFileName:self.name path:@"" size:self.size index:0 torrent:self];
|
|
fFileList = @[ node ];
|
|
fFlatFileList = fFileList;
|
|
}
|
|
}
|
|
|
|
- (void)insertPathForComponents:(NSArray*)components
|
|
withComponentIndex:(NSUInteger)componentIndex
|
|
forParent:(FileListNode*)parent
|
|
fileSize:(uint64_t)size
|
|
index:(NSInteger)index
|
|
flatList:(NSMutableArray*)flatFileList
|
|
{
|
|
NSParameterAssert(components.count > 0);
|
|
NSParameterAssert(componentIndex < components.count);
|
|
|
|
NSString* name = components[componentIndex];
|
|
BOOL const isFolder = componentIndex < (components.count - 1);
|
|
|
|
//determine if folder node already exists
|
|
__block FileListNode* node = nil;
|
|
if (isFolder)
|
|
{
|
|
[parent.children enumerateObjectsWithOptions:NSEnumerationConcurrent
|
|
usingBlock:^(FileListNode* searchNode, NSUInteger idx, BOOL* stop) {
|
|
if ([searchNode.name isEqualToString:name] && searchNode.isFolder)
|
|
{
|
|
node = searchNode;
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
}
|
|
|
|
//create new folder or file if it doesn't already exist
|
|
if (!node)
|
|
{
|
|
NSString* path = [parent.path stringByAppendingPathComponent:parent.name];
|
|
if (isFolder)
|
|
{
|
|
node = [[FileListNode alloc] initWithFolderName:name path:path torrent:self];
|
|
}
|
|
else
|
|
{
|
|
node = [[FileListNode alloc] initWithFileName:name path:path size:size index:index torrent:self];
|
|
[flatFileList addObject:node];
|
|
}
|
|
|
|
[parent insertChild:node];
|
|
}
|
|
|
|
if (isFolder)
|
|
{
|
|
[node insertIndex:index withSize:size];
|
|
|
|
[self insertPathForComponents:components withComponentIndex:(componentIndex + 1) forParent:node fileSize:size
|
|
index:index
|
|
flatList:flatFileList];
|
|
}
|
|
}
|
|
|
|
- (void)sortFileList:(NSMutableArray*)fileNodes
|
|
{
|
|
NSSortDescriptor* descriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES
|
|
selector:@selector(localizedStandardCompare:)];
|
|
[fileNodes sortUsingDescriptors:@[ descriptor ]];
|
|
|
|
[fileNodes enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(FileListNode* node, NSUInteger idx, BOOL* stop) {
|
|
if (node.isFolder)
|
|
[self sortFileList:node.children];
|
|
}];
|
|
}
|
|
|
|
- (void)startQueue
|
|
{
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"UpdateQueue" object:self];
|
|
}
|
|
|
|
- (void)completenessChange:(tr_completeness)status wasRunning:(BOOL)wasRunning
|
|
{
|
|
fStat = tr_torrentStat(fHandle); //don't call update yet to avoid auto-stop
|
|
|
|
switch (status)
|
|
{
|
|
case TR_SEED:
|
|
case TR_PARTIAL_SEED:
|
|
{
|
|
NSDictionary* statusInfo = @{@"Status" : @(status), @"WasRunning" : @(wasRunning)};
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"TorrentFinishedDownloading" object:self userInfo:statusInfo];
|
|
|
|
//quarantine the finished data
|
|
NSString* dataLocation = [self.currentDirectory stringByAppendingPathComponent:self.name];
|
|
NSURL* dataLocationUrl = [NSURL fileURLWithPath:dataLocation];
|
|
NSDictionary* quarantineProperties = @{
|
|
(NSString*)kLSQuarantineTypeKey : (NSString*)kLSQuarantineTypeOtherDownload
|
|
};
|
|
NSError* error = nil;
|
|
if (![dataLocationUrl setResourceValue:quarantineProperties forKey:NSURLQuarantinePropertiesKey error:&error])
|
|
{
|
|
NSLog(@"Failed to quarantine %@: %@", dataLocation, error.description);
|
|
}
|
|
break;
|
|
}
|
|
case TR_LEECH:
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"TorrentRestartedDownloading" object:self];
|
|
break;
|
|
}
|
|
|
|
[self update];
|
|
[self updateTimeMachineExclude];
|
|
}
|
|
|
|
- (void)ratioLimitHit
|
|
{
|
|
fStat = tr_torrentStat(fHandle);
|
|
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"TorrentFinishedSeeding" object:self];
|
|
}
|
|
|
|
- (void)idleLimitHit
|
|
{
|
|
fStat = tr_torrentStat(fHandle);
|
|
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"TorrentFinishedSeeding" object:self];
|
|
}
|
|
|
|
- (void)metadataRetrieved
|
|
{
|
|
fStat = tr_torrentStat(fHandle);
|
|
|
|
[self createFileList];
|
|
|
|
/* If the torrent is in no group, or the group was automatically determined based on criteria evaluated
|
|
* before we had metadata for this torrent, redetermine the group
|
|
*/
|
|
if ((fGroupValueDetermination == TorrentDeterminationAutomatic) || (self.groupValue == -1))
|
|
{
|
|
[self setGroupValue:[GroupsController.groups groupIndexForTorrent:self] determinationType:TorrentDeterminationAutomatic];
|
|
}
|
|
|
|
//change the location if the group calls for it and it's either not already set or was set automatically before
|
|
if (((fDownloadFolderDetermination == TorrentDeterminationAutomatic) || !tr_torrentGetCurrentDir(fHandle)) &&
|
|
[GroupsController.groups usesCustomDownloadLocationForIndex:self.groupValue])
|
|
{
|
|
NSString* location = [GroupsController.groups customDownloadLocationForIndex:self.groupValue];
|
|
[self changeDownloadFolderBeforeUsing:location determinationType:TorrentDeterminationAutomatic];
|
|
}
|
|
|
|
[NSNotificationCenter.defaultCenter postNotificationName:@"ResetInspector" object:self userInfo:@{ @"Torrent" : self }];
|
|
}
|
|
|
|
- (void)renameFinished:(BOOL)success
|
|
nodes:(NSArray*)nodes
|
|
completionHandler:(void (^)(BOOL))completionHandler
|
|
oldPath:(NSString*)oldPath
|
|
newName:(NSString*)newName
|
|
{
|
|
NSParameterAssert(completionHandler != nil);
|
|
NSParameterAssert(oldPath != nil);
|
|
NSParameterAssert(newName != nil);
|
|
|
|
NSString* path = oldPath.stringByDeletingLastPathComponent;
|
|
|
|
if (success)
|
|
{
|
|
using WeakUpdateNodeAndChildrenForRename = void (^__block __weak)(FileListNode*);
|
|
using UpdateNodeAndChildrenForRename = void (^)(FileListNode*);
|
|
|
|
NSString* oldName = oldPath.lastPathComponent;
|
|
WeakUpdateNodeAndChildrenForRename weakUpdateNodeAndChildrenForRename;
|
|
UpdateNodeAndChildrenForRename updateNodeAndChildrenForRename;
|
|
weakUpdateNodeAndChildrenForRename = updateNodeAndChildrenForRename = ^(FileListNode* node) {
|
|
[node updateFromOldName:oldName toNewName:newName inPath:path];
|
|
|
|
if (node.isFolder)
|
|
{
|
|
[node.children enumerateObjectsWithOptions:NSEnumerationConcurrent
|
|
usingBlock:^(FileListNode* childNode, NSUInteger idx, BOOL* stop) {
|
|
weakUpdateNodeAndChildrenForRename(childNode);
|
|
}];
|
|
}
|
|
};
|
|
|
|
if (!nodes)
|
|
{
|
|
nodes = fFlatFileList;
|
|
}
|
|
[nodes enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(FileListNode* node, NSUInteger idx, BOOL* stop) {
|
|
updateNodeAndChildrenForRename(node);
|
|
}];
|
|
|
|
//resort lists
|
|
NSMutableArray* fileList = [fFileList mutableCopy];
|
|
[self sortFileList:fileList];
|
|
fFileList = fileList;
|
|
|
|
NSMutableArray* flatFileList = [fFlatFileList mutableCopy];
|
|
[self sortFileList:flatFileList];
|
|
fFlatFileList = flatFileList;
|
|
|
|
fIcon = nil;
|
|
}
|
|
else
|
|
{
|
|
NSLog(@"Error renaming %@ to %@", oldPath, [path stringByAppendingPathComponent:newName]);
|
|
}
|
|
|
|
completionHandler(success);
|
|
}
|
|
|
|
- (BOOL)shouldShowEta
|
|
{
|
|
if (fStat->activity == TR_STATUS_DOWNLOAD)
|
|
{
|
|
return YES;
|
|
}
|
|
else if (self.seeding)
|
|
{
|
|
//ratio: show if it's set at all
|
|
if (tr_torrentGetSeedRatio(fHandle, NULL))
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
//idle: show only if remaining time is less than cap
|
|
if (fStat->etaIdle != TR_ETA_NOT_AVAIL && fStat->etaIdle < ETA_IDLE_DISPLAY_SEC)
|
|
{
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (NSString*)etaString
|
|
{
|
|
NSInteger eta;
|
|
BOOL fromIdle;
|
|
//don't check for both, since if there's a regular ETA, the torrent isn't idle so it's meaningless
|
|
if (fStat->eta != TR_ETA_NOT_AVAIL && fStat->eta != TR_ETA_UNKNOWN)
|
|
{
|
|
eta = fStat->eta;
|
|
fromIdle = NO;
|
|
}
|
|
else if (fStat->etaIdle != TR_ETA_NOT_AVAIL && fStat->etaIdle < ETA_IDLE_DISPLAY_SEC)
|
|
{
|
|
eta = fStat->etaIdle;
|
|
fromIdle = YES;
|
|
}
|
|
else
|
|
{
|
|
return NSLocalizedString(@"remaining time unknown", "Torrent -> eta string");
|
|
}
|
|
|
|
static NSDateComponentsFormatter* formatter;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
formatter = [NSDateComponentsFormatter new];
|
|
formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleShort;
|
|
formatter.maximumUnitCount = 2;
|
|
formatter.collapsesLargestUnit = YES;
|
|
formatter.includesTimeRemainingPhrase = YES;
|
|
});
|
|
NSString* idleString = [formatter stringFromTimeInterval:eta];
|
|
|
|
if (fromIdle)
|
|
{
|
|
idleString = [idleString stringByAppendingFormat:@" (%@)", NSLocalizedString(@"inactive", "Torrent -> eta string")];
|
|
}
|
|
|
|
return idleString;
|
|
}
|
|
|
|
- (void)setTimeMachineExclude:(BOOL)exclude
|
|
{
|
|
NSString* path;
|
|
if ((path = self.dataLocation))
|
|
{
|
|
CSBackupSetItemExcluded((__bridge CFURLRef)[NSURL fileURLWithPath:path], exclude, false);
|
|
fTimeMachineExcludeInitialized = YES;
|
|
}
|
|
}
|
|
|
|
@end
|