From 7e3ce5c48d9daddab6dd5454be879fed985f8793 Mon Sep 17 00:00:00 2001 From: Ben Davis Date: Sun, 13 Aug 2017 19:49:21 +0100 Subject: [PATCH] Added some speed trackers --- BitTorrent.xcodeproj/project.pbxproj | 24 +++- BitTorrent/Models/BitField.swift | 5 +- BitTorrent/Models/BitFieldTests.swift | 12 ++ .../Peer Manager/TorrentPeerManager.swift | 8 +- .../TorrentPeerManagerTests.swift | 16 +++ BitTorrent/Peer/TorrentPeer.swift | 4 + BitTorrent/Peer/TorrentPeerTests.swift | 12 ++ BitTorrent/Torrent Client/TorrentClient.swift | 10 +- .../Utilities/NetworkSpeedTracker.swift | 62 +++++++++ .../Utilities/NetworkSpeedTrackerTests.swift | 53 ++++++++ BitTorrentExample/StringUtilities.swift | 63 +++++++++ BitTorrentExample/TorrentInfoRowData.swift | 88 +++++++++++++ BitTorrentExample/TorrentViewController.swift | 124 +++++++++--------- 13 files changed, 406 insertions(+), 75 deletions(-) create mode 100644 BitTorrent/Utilities/NetworkSpeedTracker.swift create mode 100644 BitTorrent/Utilities/NetworkSpeedTrackerTests.swift create mode 100644 BitTorrentExample/StringUtilities.swift create mode 100644 BitTorrentExample/TorrentInfoRowData.swift diff --git a/BitTorrent.xcodeproj/project.pbxproj b/BitTorrent.xcodeproj/project.pbxproj index b4e3104..7ff3323 100644 --- a/BitTorrent.xcodeproj/project.pbxproj +++ b/BitTorrent.xcodeproj/project.pbxproj @@ -13,11 +13,14 @@ B50B24F71F0A553F00C23E7C /* UDPConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50B24F61F0A553B00C23E7C /* UDPConnectionTests.swift */; }; B50B24F91F0A554A00C23E7C /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50B24F81F0A554A00C23E7C /* UDPConnection.swift */; }; B514DD7B1F40971D00C932F8 /* TorrentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD7A1F40971D00C932F8 /* TorrentViewController.swift */; }; - B514DD801F409E2200C932F8 /* BitByteString.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD7F1F409E2200C932F8 /* BitByteString.swift */; }; B514DD831F40A2B800C932F8 /* TrackerManagerTests.torrent in Resources */ = {isa = PBXBuildFile; fileRef = B514DD821F40A2B800C932F8 /* TrackerManagerTests.torrent */; }; B514DD851F40ABB300C932F8 /* TorrentClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD841F40ABB300C932F8 /* TorrentClientTests.swift */; }; B514DD881F40B2EC00C932F8 /* TorrentClientManagerStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD861F40B2C700C932F8 /* TorrentClientManagerStubs.swift */; }; B514DD891F40B58200C932F8 /* text.txt in Resources */ = {isa = PBXBuildFile; fileRef = B5E9B0E41F02F9E700EF58E3 /* text.txt */; }; + B514DD8B1F40C40500C932F8 /* NetworkSpeedTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD8A1F40C40500C932F8 /* NetworkSpeedTracker.swift */; }; + B514DD8F1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */; }; + B514DD911F40D15C00C932F8 /* StringUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD901F40D15C00C932F8 /* StringUtilities.swift */; }; + B514DD931F40D27500C932F8 /* TorrentInfoRowData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514DD921F40D27500C932F8 /* TorrentInfoRowData.swift */; }; B51638931F0EE9B6009E563E /* TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638921F0EE9B6009E563E /* TCPConnection.swift */; }; B51638951F0EEAA4009E563E /* TCPConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638941F0EEAA4009E563E /* TCPConnectionTests.swift */; }; B51638971F0EEC2B009E563E /* GCDAsyncSocketStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638961F0EEC2B009E563E /* GCDAsyncSocketStub.swift */; }; @@ -163,10 +166,13 @@ B50B24F61F0A553B00C23E7C /* UDPConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnectionTests.swift; sourceTree = ""; }; B50B24F81F0A554A00C23E7C /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = ""; }; B514DD7A1F40971D00C932F8 /* TorrentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentViewController.swift; sourceTree = ""; }; - B514DD7F1F409E2200C932F8 /* BitByteString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BitByteString.swift; path = ../../BitTorrent_one/BitTorrent/BitByteString.swift; sourceTree = ""; }; B514DD821F40A2B800C932F8 /* TrackerManagerTests.torrent */ = {isa = PBXFileReference; lastKnownFileType = file; path = TrackerManagerTests.torrent; sourceTree = ""; }; B514DD841F40ABB300C932F8 /* TorrentClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentClientTests.swift; sourceTree = ""; }; B514DD861F40B2C700C932F8 /* TorrentClientManagerStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentClientManagerStubs.swift; sourceTree = ""; }; + B514DD8A1F40C40500C932F8 /* NetworkSpeedTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSpeedTracker.swift; sourceTree = ""; }; + B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSpeedTrackerTests.swift; sourceTree = ""; }; + B514DD901F40D15C00C932F8 /* StringUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtilities.swift; sourceTree = ""; }; + B514DD921F40D27500C932F8 /* TorrentInfoRowData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentInfoRowData.swift; sourceTree = ""; }; B51638921F0EE9B6009E563E /* TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnection.swift; sourceTree = ""; }; B51638941F0EEAA4009E563E /* TCPConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnectionTests.swift; sourceTree = ""; }; B51638961F0EEC2B009E563E /* GCDAsyncSocketStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCDAsyncSocketStub.swift; sourceTree = ""; }; @@ -192,7 +198,7 @@ B54D0C141CA53983004343BD /* BEncode.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = BEncode.xcodeproj; path = Submodules/BEncodeSwift/BEncode.xcodeproj; sourceTree = ""; }; B54D0C231CA56A22004343BD /* TorrentMetaInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TorrentMetaInfo.swift; sourceTree = ""; }; B54D0C261CA56ADB004343BD /* TorrentMetaInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TorrentMetaInfoTests.swift; sourceTree = ""; }; - B54D0C2B1CA5787E004343BD /* Data+sha1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Data+sha1.swift"; path = "Utilities/Data+sha1.swift"; sourceTree = ""; }; + B54D0C2B1CA5787E004343BD /* Data+sha1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+sha1.swift"; sourceTree = ""; }; B54D0C581CA58722004343BD /* CommonCrypto.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CommonCrypto.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B54D0C6F1CA58749004343BD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B54D0C711CA58767004343BD /* CommonCrypto.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = CommonCrypto.xcconfig; sourceTree = ""; }; @@ -385,10 +391,11 @@ B54D0C291CA5785F004343BD /* Utilities */ = { isa = PBXGroup; children = ( - B514DD7F1F409E2200C932F8 /* BitByteString.swift */, B54D0C2B1CA5787E004343BD /* Data+sha1.swift */, + B514DD8A1F40C40500C932F8 /* NetworkSpeedTracker.swift */, + B514DD8E1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift */, ); - name = Utilities; + path = Utilities; sourceTree = ""; }; B54D0C591CA58722004343BD /* CommonCrypto */ = { @@ -544,6 +551,8 @@ children = ( B5E9B0D71F02E6F800EF58E3 /* AppDelegate.swift */, B514DD7A1F40971D00C932F8 /* TorrentViewController.swift */, + B514DD921F40D27500C932F8 /* TorrentInfoRowData.swift */, + B514DD901F40D15C00C932F8 /* StringUtilities.swift */, B5E9B0D91F02E6F800EF58E3 /* Assets.xcassets */, B5E9B0DB1F02E6F800EF58E3 /* LaunchScreen.storyboard */, B5E9B0DE1F02E6F800EF58E3 /* Info.plist */, @@ -948,7 +957,7 @@ B55317DC1F02FC4D00909ADF /* TorrentHTTPTracker.swift in Sources */, B52EE3401F129CA200AC22D6 /* TorrentPeerHandshakeMessageBuffer.swift in Sources */, B5F27C621F3F93AC0040589C /* TorrentProgressManager.swift in Sources */, - B514DD801F409E2200C932F8 /* BitByteString.swift in Sources */, + B514DD8B1F40C40500C932F8 /* NetworkSpeedTracker.swift in Sources */, B5E977961CAFB46B0038EBE7 /* String+URLEncode.swift in Sources */, B558F4831F0A647D00438BB4 /* InternetProtocol.swift in Sources */, B527F62B1F1BD5FA001F06AF /* TorrentPieceDownloadBuffer.swift in Sources */, @@ -986,6 +995,7 @@ B556D5AE1F0BA1FD00277B8D /* GCDAsyncUdpSocketStub.swift in Sources */, B5AF7AAD1F253055003FD66F /* FileHandleFake.swift in Sources */, B5530DB51F03063E00F71CCD /* HTTPConnectionTests.swift in Sources */, + B514DD8F1F40C53C00C932F8 /* NetworkSpeedTrackerTests.swift in Sources */, B535533C1F0FEE6300A6DCBE /* TCPConnectionStub.swift in Sources */, B5C5F9651F2500D1007623B2 /* TorrentProgressTests.swift in Sources */, B52EE3431F129CB000AC22D6 /* TorrentPeerHandshakeMessageBufferTests.swift in Sources */, @@ -1015,7 +1025,9 @@ buildActionMask = 2147483647; files = ( B5E9B0D81F02E6F800EF58E3 /* AppDelegate.swift in Sources */, + B514DD911F40D15C00C932F8 /* StringUtilities.swift in Sources */, B514DD7B1F40971D00C932F8 /* TorrentViewController.swift in Sources */, + B514DD931F40D27500C932F8 /* TorrentInfoRowData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BitTorrent/Models/BitField.swift b/BitTorrent/Models/BitField.swift index 3f5872c..026f8da 100644 --- a/BitTorrent/Models/BitField.swift +++ b/BitTorrent/Models/BitField.swift @@ -11,6 +11,9 @@ import Foundation public struct BitField: Equatable { public let size: Int public private(set) var value: [Bool] + public var complete: Bool { + return !contains(where: { !$0.isSet }) + } init(size: Int) { self.size = size @@ -90,7 +93,7 @@ public struct BitField: Equatable { } extension BitField: Sequence { - public func makeIterator() -> AnyIterator<(Int, Bool)> { + public func makeIterator() -> AnyIterator<(index: Int, isSet: Bool)> { var index = 0 return AnyIterator { guard index < self.size else { return nil } diff --git a/BitTorrent/Models/BitFieldTests.swift b/BitTorrent/Models/BitFieldTests.swift index 6e7c60d..5b1506d 100644 --- a/BitTorrent/Models/BitFieldTests.swift +++ b/BitTorrent/Models/BitFieldTests.swift @@ -96,4 +96,16 @@ class BitFieldTests: XCTestCase { XCTAssertEqual(indices, [0,1,2,3,4]) XCTAssertEqual(values, [false,false,true,true,false]) } + + func test_allBitsSet() { + var example = BitField(size: 5) + example.set(at: 0) + example.set(at: 1) + example.set(at: 2) + example.set(at: 3) + + XCTAssertFalse(example.complete) + example.set(at: 4) + XCTAssertTrue(example.complete) + } } diff --git a/BitTorrent/Peer Manager/TorrentPeerManager.swift b/BitTorrent/Peer Manager/TorrentPeerManager.swift index 79adba6..a95175c 100644 --- a/BitTorrent/Peer Manager/TorrentPeerManager.swift +++ b/BitTorrent/Peer Manager/TorrentPeerManager.swift @@ -42,9 +42,14 @@ class TorrentPeerManager { let bitFieldSize: Int private(set) var peers: [TorrentPeer] = [] - private var numberOfConnectedPeers: Int { + var numberOfConnectedPeers: Int { return peers.filter({ $0.connected }).count } + var numberOfConnectedSeeds: Int { + return peers.filter({ $0.connected && $0.currentProgress.complete }).count + } + + private(set) var downloadSpeedTracker = NetworkSpeedTracker () init(clientId: Data, infoHash: Data, bitFieldSize: Int) { self.clientId = clientId @@ -113,6 +118,7 @@ extension TorrentPeerManager: TorrentPeerDelegate { } func peer(_ sender: TorrentPeer, gotPieceAtIndex index: Int, piece: Data) { + downloadSpeedTracker.increase(by: piece.count) delegate?.torrentPeerManager(self, downloadedPieceAtIndex: index, piece: piece) requestNextPiece(from: sender) // TODO: send have to peers diff --git a/BitTorrent/Peer Manager/TorrentPeerManagerTests.swift b/BitTorrent/Peer Manager/TorrentPeerManagerTests.swift index 6ace611..90f2b5c 100644 --- a/BitTorrent/Peer Manager/TorrentPeerManagerTests.swift +++ b/BitTorrent/Peer Manager/TorrentPeerManagerTests.swift @@ -282,4 +282,20 @@ class TorrentPeerManagerTests: XCTestCase { // Then XCTAssertFalse(peer.downloadPieceCalled) } + + func test_downloadSpeedRecordedOnGettingPiece() { + + // Given + let peerInfo = TorrentPeerInfo(ip: "127.0.0.1", port: 123, peerId: nil) + sut.addPeers(withInfo: [peerInfo]) + guard let peer = peers.first else { return } + + let pieceSize = 100 + + // When + sut.peer(peer, gotPieceAtIndex: 0, piece: Data(repeating: 0, count: pieceSize)) + + // Then + XCTAssertEqual(sut.downloadSpeedTracker.totalNumberOfBytes, pieceSize) + } } diff --git a/BitTorrent/Peer/TorrentPeer.swift b/BitTorrent/Peer/TorrentPeer.swift index e8d25a3..9b83387 100644 --- a/BitTorrent/Peer/TorrentPeer.swift +++ b/BitTorrent/Peer/TorrentPeer.swift @@ -38,7 +38,10 @@ class TorrentPeer { private(set) var peerInterested: Bool = false private(set) var amChokedToPeer: Bool = true private(set) var amInterestedInPeer: Bool = false + private(set) var currentProgress: BitField + private(set) var downloadSpeedTracker = NetworkSpeedTracker() + private(set) var uploadSpeedTracker = NetworkSpeedTracker() private var downloadPieceRequests: [Int: TorrentPieceDownloadBuffer] = [:] private var numberOfPendingBlockRequests = 0 @@ -205,6 +208,7 @@ extension TorrentPeer: TorrentPeerCommunicatorDelegate { } func peer(_ sender: TorrentPeerCommunicator, sentPiece index: Int, begin: Int, block: Data) { + downloadSpeedTracker.increase(by: block.count) guard let downloadPieceBuffer = downloadPieceRequests[index] else { return } numberOfPendingBlockRequests -= 1 downloadPieceBuffer.gotBlock(block, begin: begin) diff --git a/BitTorrent/Peer/TorrentPeerTests.swift b/BitTorrent/Peer/TorrentPeerTests.swift index a376c1a..479fc63 100644 --- a/BitTorrent/Peer/TorrentPeerTests.swift +++ b/BitTorrent/Peer/TorrentPeerTests.swift @@ -352,4 +352,16 @@ class TorrentPeerTests: XCTestCase { } waitForExpectations(timeout: 0.1) } + + // MARK: - Speed trackers + + func test_gotPieceRecordedInSpeedTracker() { + communicator.sendRequestParameters = [] + communicator.delegate?.peer(communicator, + sentPiece: pieceIndex, + begin: 0, + block: Data(repeating: 0, count: pieceSize)) + + XCTAssertEqual(sut.downloadSpeedTracker.totalNumberOfBytes, pieceSize) + } } diff --git a/BitTorrent/Torrent Client/TorrentClient.swift b/BitTorrent/Torrent Client/TorrentClient.swift index 27600a4..5cd457f 100644 --- a/BitTorrent/Torrent Client/TorrentClient.swift +++ b/BitTorrent/Torrent Client/TorrentClient.swift @@ -31,12 +31,12 @@ public class TorrentClient { public let metaInfo: TorrentMetaInfo - public var progress: TorrentProgress { - return progressManager.progress - } - public private(set) var status: Status = .stopped - + public var progress: TorrentProgress { return progressManager.progress } + public var numberOfConnectedPeers: Int { return peerManager.numberOfConnectedPeers } + public var numberOfConnectedSeeds: Int { return peerManager.numberOfConnectedPeers } + public var downloadSpeedTracker: NetworkSpeedTracker { return peerManager.downloadSpeedTracker } + let progressManager: TorrentProgressManager let peerManager: TorrentPeerManager let trackerManager: TorrentTrackerManager diff --git a/BitTorrent/Utilities/NetworkSpeedTracker.swift b/BitTorrent/Utilities/NetworkSpeedTracker.swift new file mode 100644 index 0000000..be3192a --- /dev/null +++ b/BitTorrent/Utilities/NetworkSpeedTracker.swift @@ -0,0 +1,62 @@ +// +// NetworkSpeedTracker.swift +// BitTorrent +// +// Created by Ben Davis on 13/08/2017. +// Copyright © 2017 Ben Davis. All rights reserved. +// + +import Foundation + +extension Collection { + func firstWhere(_ predicate: (Element) -> Bool) -> Element? { + return first(where: predicate) + } +} + +public struct NetworkSpeedDataPoint: Equatable, Comparable { + + var numberOfBytes: Int + var dateRecorded: Date + + init(_ numberOfBytes: Int, dateRecorded: Date = Date()) { + self.numberOfBytes = numberOfBytes + self.dateRecorded = dateRecorded + } + + public static func ==(_ lhs: NetworkSpeedDataPoint, _ rhs: NetworkSpeedDataPoint) -> Bool { + return ( + lhs.numberOfBytes == rhs.numberOfBytes && + lhs.dateRecorded == rhs.dateRecorded + ) + } + + public static func <(lhs: NetworkSpeedDataPoint, rhs: NetworkSpeedDataPoint) -> Bool { + return lhs.dateRecorded.compare(rhs.dateRecorded) == .orderedAscending + } +} + +public struct NetworkSpeedTracker { + var totalNumberOfBytes: Int = 0 + private var dataPoints: [NetworkSpeedDataPoint] = [NetworkSpeedDataPoint(0)] + + mutating func increase(by bytes: Int) { + totalNumberOfBytes += bytes + addDataPoint(NetworkSpeedDataPoint(totalNumberOfBytes)) + } + + private mutating func addDataPoint(_ dataPoint: NetworkSpeedDataPoint) { + dataPoints = [dataPoint] + dataPoints + } + + public func numberOfBytesDownloaded(since date: Date) -> Int { + guard let previouslyDataPoint = dataPoints.firstWhere({ $0.dateRecorded < date }) else { + return totalNumberOfBytes + } + return totalNumberOfBytes - previouslyDataPoint.numberOfBytes + } + + public func numberOfBytesDownloaded(over timeInterval: TimeInterval) -> Int { + return numberOfBytesDownloaded(since: Date(timeIntervalSinceNow: -timeInterval)) + } +} diff --git a/BitTorrent/Utilities/NetworkSpeedTrackerTests.swift b/BitTorrent/Utilities/NetworkSpeedTrackerTests.swift new file mode 100644 index 0000000..5761c8e --- /dev/null +++ b/BitTorrent/Utilities/NetworkSpeedTrackerTests.swift @@ -0,0 +1,53 @@ +// +// NetworkSpeedTrackerTests.swift +// BitTorrentTests +// +// Created by Ben Davis on 13/08/2017. +// Copyright © 2017 Ben Davis. All rights reserved. +// + +import XCTest +@testable import BitTorrent + +class NetworkSpeedTrackerTests: XCTestCase { + + func test_increaseBytes() { + var sut = NetworkSpeedTracker() + sut.increase(by: 10) + XCTAssertEqual(sut.totalNumberOfBytes, 10) + } + + func test_canGetBytesDownloadedSinceDate() { + var sut = NetworkSpeedTracker() + sut.increase(by: 2) + let date = Date() + sut.increase(by: 10) + sut.increase(by: 5) + XCTAssertEqual(sut.numberOfBytesDownloaded(since: date), 15) + } + + func test_0BytesIfNoDataRecrodedSince() { + var sut = NetworkSpeedTracker() + sut.increase(by: 2) + let date = Date() + XCTAssertEqual(sut.numberOfBytesDownloaded(since: date), 0) + } + + func test_canGetBytesOverAllTime() { + let date = Date() + var sut = NetworkSpeedTracker() + sut.increase(by: 10) + sut.increase(by: 5) + XCTAssertEqual(sut.numberOfBytesDownloaded(since: date), 15) + } + + func test_canGetBytesDownloadedOverTimePeriod() { + var sut = NetworkSpeedTracker() + sut.increase(by: 2) + usleep(2000) + sut.increase(by: 10) + sut.increase(by: 5) + let timePeriod: TimeInterval = 0.002 + XCTAssertEqual(sut.numberOfBytesDownloaded(over: timePeriod), 15) + } +} diff --git a/BitTorrentExample/StringUtilities.swift b/BitTorrentExample/StringUtilities.swift new file mode 100644 index 0000000..d670a36 --- /dev/null +++ b/BitTorrentExample/StringUtilities.swift @@ -0,0 +1,63 @@ +// +// StringUtilities.swift +// BitTorrentExample +// +// Created by Ben Davis on 13/08/2017. +// Copyright © 2017 Ben Davis. All rights reserved. +// + +import Foundation + +extension TimeInterval { + + var milliseconds: Int { + return Int((self.truncatingRemainder(dividingBy: 1)) * 1000) + } + + var seconds: Int { + return Int(self.remainder(dividingBy: 60)) + } + + var minutes: Int { + return Int((self/60).remainder(dividingBy: 60)) + } + + var hours: Int { + return Int(self / (60*60)) + } + + var stringTime: String { + if self.hours != 0 { + return "\(self.hours)h \(self.minutes)m \(self.seconds)s" + } else if self.minutes != 0 { + return "\(self.minutes)m \(self.seconds)s" + } else if self.milliseconds != 0 { + return "\(self.seconds)s \(self.milliseconds)ms" + } else { + return "\(self.seconds)s" + } + } +} + +public let bytesInKB = 1024 +public let bytesInMB = bytesInKB*1024 +public let bytesInGB = bytesInMB*1024 + +public func twoDecimalPlaceFloat(_ float: Float) -> String { + return String(format: "%.2f", float) +} + +public func bytesToString(_ numberOfBytes: Int) -> String { + if (numberOfBytes > bytesInGB) { + let numberOfGBs: Float = Float(numberOfBytes) / Float(bytesInGB) + return "\(twoDecimalPlaceFloat(numberOfGBs)) GB" + } else if (numberOfBytes > bytesInMB) { + let numberOfMBs: Float = Float(numberOfBytes) / Float(bytesInMB) + return "\(twoDecimalPlaceFloat(numberOfMBs)) MB" + } else if (numberOfBytes > bytesInKB) { + let numberOfKBs: Float = Float(numberOfBytes) / Float(bytesInKB) + return "\(twoDecimalPlaceFloat(numberOfKBs)) KB" + } else { + return "\(numberOfBytes) Bytes" + } +} diff --git a/BitTorrentExample/TorrentInfoRowData.swift b/BitTorrentExample/TorrentInfoRowData.swift new file mode 100644 index 0000000..c700175 --- /dev/null +++ b/BitTorrentExample/TorrentInfoRowData.swift @@ -0,0 +1,88 @@ +// +// TorrentInfoRowData.swift +// BitTorrentExample +// +// Created by Ben Davis on 13/08/2017. +// Copyright © 2017 Ben Davis. All rights reserved. +// + +import BitTorrent + +enum TorrentInfoRowData: Int { + case name = 0 + case size, percentageComplete, status, seeds, peers, downloadSpeed, uploadSpeed, eta, uploaded + + static var numberOfRows: Int = 10 + + var titleText: String { + switch self { + case .name: + return "Name" + case .size: + return "Size" + case .percentageComplete: + return "Completed" + case .status: + return "Status" + case .seeds: + return "Seeds" + case .peers: + return "Peers" + case .downloadSpeed: + return "↓ Speed" + case .uploadSpeed: + return "↑ Speed" + case .eta: + return "ETA" + case .uploaded: + return "Uploaded" + } + } + + func value(using client: TorrentClient) -> String { + + let speedSampleSize: TimeInterval = 5 + + switch self { + + case .name: + return client.metaInfo.info.name + + case .size: + return bytesToString(client.metaInfo.info.length) + + case .percentageComplete: + let percentageComplete = client.progress.percentageComplete + let progressString = twoDecimalPlaceFloat(percentageComplete * 100) + return "\(progressString)%" + + case .status: + return client.status.toString + + case .seeds: + return "\(client.numberOfConnectedSeeds)" + + case .peers: + return "\(client.numberOfConnectedPeers)" + + case .downloadSpeed: + let speed = client.downloadSpeedTracker.numberOfBytesDownloaded(over: speedSampleSize) + return bytesToString(speed / Int(speedSampleSize)) + "/s" + + case .uploadSpeed: + return "????" + + case .eta: + let speed = client.downloadSpeedTracker.numberOfBytesDownloaded(over: speedSampleSize) + guard speed > 0 else { return "∞" } + + let remaining = client.progress.remaining * client.metaInfo.info.pieceLength + guard speed > 0 else { return "n/a" } + + return TimeInterval(remaining / speed).stringTime + + case .uploaded: + return bytesToString(client.progress.uploaded * client.metaInfo.info.pieceLength) + } + } +} diff --git a/BitTorrentExample/TorrentViewController.swift b/BitTorrentExample/TorrentViewController.swift index 78ffc9b..145b26d 100644 --- a/BitTorrentExample/TorrentViewController.swift +++ b/BitTorrentExample/TorrentViewController.swift @@ -11,9 +11,19 @@ import BitTorrent class TorrentViewController: UIViewController { + static let refreshRate: TimeInterval = 1 + let tableView = UITableView() let torrentClient: TorrentClient + lazy var refreshTimer: Timer = { + return Timer.scheduledTimer(timeInterval: TorrentViewController.refreshRate, + target: self, + selector: #selector(TorrentViewController.timerFired), + userInfo: nil, + repeats: true) + }() + init(torrentClient: TorrentClient) { self.torrentClient = torrentClient super.init(nibName: nil, bundle: nil) @@ -26,6 +36,7 @@ class TorrentViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self + tableView.delegate = self view.addSubview(tableView) } @@ -37,77 +48,44 @@ class TorrentViewController: UIViewController { tableView.contentOffset = .zero } } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + refreshTimer.fire() + } + + @objc private func timerFired() { + tableView.reloadData() + } } extension TorrentViewController: UITableViewDataSource { - enum TableViewRow: Int { - case name = 0 - case size, percentageComplete, status, seeds, peers, downloadSpeed, uploadSpeed, eta, uploaded - - static var numberOfRows: Int = 10 - - var titleText: String { - switch self { - case .name: - return "Name" - case .size: - return "Size" - case .percentageComplete: - return "Completed" - case .status: - return "Status" - case .seeds: - return "Seeds" - case .peers: - return "Peers" - case .downloadSpeed: - return "↓ Speed" - case .uploadSpeed: - return "↑ Speed" - case .eta: - return "ETA" - case .uploaded: - return "Uploaded" - } - } - - func value(using client: TorrentClient) -> String { - switch self { - case .name: - return client.metaInfo.info.name - case .size: - return bytesToString(client.metaInfo.info.length) - case .percentageComplete: - let percentageComplete = client.progress.percentageComplete - let progressString = twoDecimalPlaceFloat(percentageComplete * 100) - return "\(progressString)%" - case .status: - return client.status.toString -// case .seeds: -// return "Seeds" -// case .peers: -// return "Peers" -// case .downloadSpeed: -// return "↓ Speed" -// case .uploadSpeed: -// return "↑ Speed" -// case .eta: -// return "ETA" -// case .uploaded: -// return "Uploaded" - default: - return "????" - } - } + func numberOfSections(in tableView: UITableView) -> Int { + return torrentClient.status == .stopped ? 2 : 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return TableViewRow.numberOfRows + if section == 0 { + return TorrentInfoRowData.numberOfRows + } else { + return 1 + } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - + if indexPath.section == 0 { + return cellForTorrentInfoSection(at: indexPath, tableView: tableView) + } else { + let startCell = UITableViewCell(style: .default, reuseIdentifier: nil) + startCell.textLabel?.text = "Start" + startCell.textLabel?.textAlignment = .center + startCell.textLabel?.textColor = .blue + return startCell + } + } + + func cellForTorrentInfoSection(at indexPath: IndexPath, tableView: UITableView) -> UITableViewCell { let cellReuseIdentifier = "Cell" let cell: UITableViewCell @@ -123,8 +101,30 @@ extension TorrentViewController: UITableViewDataSource { } func setupCell(_ cell: UITableViewCell, atIndexPath indexPath: IndexPath) { - guard let row = TableViewRow(rawValue: indexPath.row) else { return } + guard let row = TorrentInfoRowData(rawValue: indexPath.row) else { return } cell.textLabel?.text = row.titleText cell.detailTextLabel?.text = row.value(using: torrentClient) } } + +extension TorrentViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if indexPath.section == 0 { + return 25 + } else { + return 44 + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let cell = tableView.cellForRow(at: indexPath) else { return } + cell.setSelected(false, animated: false) + + if indexPath.section == 1 { + torrentClient.start() + tableView.reloadData() + } + } + +}