quartz_flow 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ require 'open-uri'
2
+ require 'quartz_torrent.rb'
3
+ require 'fileutils'
4
+ require 'quartz_flow/usagetracker'
5
+ require 'thread'
6
+
7
+ class TorrentManager
8
+ def initialize(peerClient, torrentFileDir, monthlyResetDay)
9
+ @peerClient = peerClient
10
+ @cachedTorrentData = nil
11
+ @cachedAt = nil
12
+ @cacheLifetime = 2
13
+ @torrentFileDir = torrentFileDir
14
+ @peerClientStopped = false
15
+ @usageTracker = UsageTracker.new(monthlyResetDay)
16
+ # Start a thread to keep track of usage.
17
+ startUsageTrackerThread
18
+ end
19
+
20
+ attr_reader :peerClient
21
+
22
+ def torrentData(infoHash = nil)
23
+ if (! @cachedTorrentData || Time.new - @cachedAt > @cacheLifetime) && ! @peerClientStopped
24
+ @cachedTorrentData = @peerClient.torrentData
25
+ @cachedAt = Time.new
26
+ end
27
+
28
+ @cachedTorrentData
29
+ end
30
+
31
+ def stopPeerClient
32
+ @peerClient.stop
33
+ @peerClientStopped = true
34
+ end
35
+
36
+ # Start torrents that already exist in the torrent file directory
37
+ def startExistingTorrents
38
+ Dir.new(@torrentFileDir).each do |e|
39
+ path = @torrentFileDir + File::SEPARATOR + e
40
+ if e =~ /\.torrent$/
41
+ puts "Starting .torrent '#{path}'"
42
+ startTorrentFile(path)
43
+ elsif e =~ /\.magnet$/
44
+ magnet = loadMagnet(path)
45
+ puts "Starting magnet '#{magnet.raw}'"
46
+ startMagnet magnet
47
+ end
48
+ end
49
+ end
50
+
51
+ # Convert torrent data such that:
52
+ # - The TorrentDataDelegate objects are converted to hashes.
53
+ def simplifiedTorrentData
54
+ result = {}
55
+ torrentData.each do |k,d|
56
+ h = d.to_h
57
+ asciiInfoHash = QuartzTorrent::bytesToHex(h[:infoHash])
58
+ h[:infoHash] = asciiInfoHash
59
+ h[:downloadRate] = QuartzTorrent::Formatter.formatSpeed(h[:downloadRate])
60
+ h[:uploadRate] = QuartzTorrent::Formatter.formatSpeed(h[:uploadRate])
61
+ h[:downloadRateDataOnly] = QuartzTorrent::Formatter.formatSpeed(h[:downloadRateDataOnly])
62
+ h[:uploadRateDataOnly] = QuartzTorrent::Formatter.formatSpeed(h[:uploadRateDataOnly])
63
+ h[:dataLength] = QuartzTorrent::Formatter.formatSize(h[:dataLength])
64
+ h[:completedBytes] = QuartzTorrent::Formatter.formatSize(h[:completedBytes])
65
+ # Sort peers
66
+ h[:peers].sort! do |a,b|
67
+ c = (b[:uploadRate].to_i <=> a[:uploadRate].to_i)
68
+ c = (b[:downloadRate].to_i <=> a[:downloadRate].to_i) if c == 0
69
+ c
70
+ end
71
+ # Format peer rates
72
+ h[:peers].each do |p|
73
+ p[:uploadRate] = QuartzTorrent::Formatter.formatSpeed(p[:uploadRate])
74
+ p[:downloadRate] = QuartzTorrent::Formatter.formatSpeed(p[:downloadRate])
75
+ end
76
+ if h[:info]
77
+ h[:info][:files].each do |file|
78
+ file[:length] = QuartzTorrent::Formatter.formatSize(file[:length])
79
+ end
80
+ end
81
+ h[:uploadRateLimit] = QuartzTorrent::Formatter.formatSpeed(h[:uploadRateLimit])
82
+ h[:downloadRateLimit] = QuartzTorrent::Formatter.formatSize(h[:downloadRateLimit])
83
+ h[:bytesUploaded] = QuartzTorrent::Formatter.formatSize(h[:bytesUploaded])
84
+ h[:bytesDownloaded] = QuartzTorrent::Formatter.formatSize(h[:bytesDownloaded])
85
+
86
+ h[:completePieces] = d.completePieceBitfield ? d.completePieceBitfield.countSet : 0
87
+ h[:totalPieces] = d.completePieceBitfield ? d.completePieceBitfield.length : 0
88
+
89
+ result[asciiInfoHash] = h
90
+ end
91
+ result
92
+ end
93
+
94
+ # Download a .torrent file from a specified URL. Return the path to the
95
+ # downloaded .torrent file.
96
+ def downloadTorrentFile(url)
97
+ # open-uri doesn't handle [ and ] properly
98
+ encodedSourcePath = URI.escape(url, /[\[\]]/)
99
+
100
+ path = nil
101
+ open(encodedSourcePath) do |f|
102
+ uriPath = f.base_uri.path
103
+ raise "The file '#{uriPath}' doesn't have the .torrent extension" if uriPath !~ /.torrent$/
104
+ path = @torrentFileDir + File::SEPARATOR + File.basename(uriPath)
105
+ File.open(path, "w"){ |outfile| outfile.write(f.read) }
106
+ end
107
+ path
108
+ end
109
+
110
+ # Store a magnet link in a file in the torrent file directory.
111
+ def storeMagnet(magnet)
112
+ asciiInfoHash = QuartzTorrent::bytesToHex(magnet.btInfoHash)
113
+ path = @torrentFileDir + File::SEPARATOR + asciiInfoHash + ".magnet"
114
+ File.open(path, "w"){ |outfile| outfile.write(magnet.raw) }
115
+ end
116
+
117
+ # Load a magnet link in a file
118
+ def loadMagnet(path)
119
+ raw = nil
120
+ File.open(path, "r"){ |infile| raw = infile.read }
121
+ QuartzTorrent::MagnetURI.new(raw)
122
+ end
123
+
124
+ # Store an uploaded .torrent file in the torrent directory. Return the path to the
125
+ # final .torrent file.
126
+ def storeUploadedTorrentFile(path, name)
127
+ name += ".torrent" if name !~ /\.torrent$/
128
+ dpath = @torrentFileDir + File::SEPARATOR + name
129
+ FileUtils.mv path, dpath
130
+ dpath
131
+ end
132
+
133
+ # Start running the torrent specified by the .torrent file given in path.
134
+ def startTorrentFile(path)
135
+ startTorrent do
136
+ begin
137
+ metainfo = QuartzTorrent::Metainfo.createFromFile(path)
138
+ @peerClient.addTorrentByMetainfo(metainfo)
139
+ rescue BEncode::DecodeError
140
+ # Delete the file
141
+ begin
142
+ FileUtils.rm path
143
+ rescue
144
+ end
145
+ raise $!
146
+ end
147
+ end
148
+ end
149
+
150
+ # Start running the magnet
151
+ def startMagnet(magnet)
152
+ startTorrent do
153
+ @peerClient.addTorrentByMagnetURI(magnet)
154
+ end
155
+ end
156
+
157
+ # Remove the specified torrent. Pass the infoHash as an ascii string, not binary.
158
+ def removeTorrent(infoHash, deleteFiles)
159
+ infoHashBytes = QuartzTorrent::hexToBytes(infoHash)
160
+ @peerClient.removeTorrent infoHashBytes, deleteFiles
161
+
162
+ # Remove torrent from torrent dir
163
+ Dir.new(@torrentFileDir).each do |e|
164
+ if e =~ /\.torrent$/
165
+ path = @torrentFileDir + File::SEPARATOR + e
166
+ metainfo = QuartzTorrent::Metainfo.createFromFile(path)
167
+ if metainfo.infoHash == infoHashBytes
168
+ FileUtils.rm path
169
+ break
170
+ end
171
+ end
172
+ end
173
+
174
+ # Remove torrent settings
175
+ helper = SettingsHelper.new
176
+ helper.deleteForOwner infoHash
177
+
178
+ # Remove magnet file if it exists
179
+ magnetFile = @torrentFileDir + File::SEPARATOR + infoHash + ".magnet"
180
+ FileUtils.rm magnetFile if File.exists?(magnetFile)
181
+ end
182
+
183
+ # Update the torrent settings (upload rate limit, etc) from database values
184
+ def applyTorrentSettings(infoHash)
185
+ asciiInfoHash = QuartzTorrent::bytesToHex(infoHash)
186
+ helper = SettingsHelper.new
187
+
188
+ # Set limits based on per-torrent settings if they exist, otherwise to default global limits if they exist.
189
+ uploadRateLimit = to_i(helper.get(:uploadRateLimit, :unfiltered, asciiInfoHash))
190
+ uploadRateLimit = to_i(helper.get(:defaultUploadRateLimit, :unfiltered)) if ! uploadRateLimit
191
+
192
+ downloadRateLimit = to_i(helper.get(:downloadRateLimit, :unfiltered, asciiInfoHash))
193
+ downloadRateLimit = to_i(helper.get(:defaultDownloadRateLimit, :unfiltered)) if ! downloadRateLimit
194
+
195
+ ratio = helper.get(:ratio, :filter, asciiInfoHash)
196
+ ratio = helper.get(:defaultRatio, :filter) if ! ratio
197
+
198
+ @peerClient.setUploadRateLimit infoHash, uploadRateLimit
199
+ @peerClient.setDownloadRateLimit infoHash, downloadRateLimit
200
+ @peerClient.setUploadRatio infoHash, ratio
201
+ end
202
+
203
+ # Get the usage for the current period of the specified type.
204
+ # periodType should be one of :daily or :monthly.
205
+ def currentPeriodUsage(periodType)
206
+ @usageTracker.currentUsage(periodType).value
207
+ end
208
+
209
+ private
210
+ # Helper for starting torrents. Expects a block that when called will add a torrent to the
211
+ # @peerClient, and return the infoHash.
212
+ def startTorrent
213
+ raise "Torrent client is shutting down" if @peerClientStopped
214
+ infoHash = yield
215
+
216
+ applyTorrentSettings infoHash
217
+ end
218
+
219
+ def to_i(val)
220
+ val = val.to_i if val
221
+ val
222
+ end
223
+
224
+ # Start a thread to keep track of usage.
225
+ def startUsageTrackerThread
226
+ @usageTrackerThread = Thread.new do
227
+ QuartzTorrent.initThread("torrent_usage_tracking")
228
+
229
+ Thread.current[:stopped] = false
230
+
231
+ while ! Thread.current[:stopped]
232
+ begin
233
+ sleep 4
234
+ torrentData = @peerClient.torrentData
235
+ usage = 0
236
+ torrentData.each do |k,v|
237
+ usage += v.bytesUploaded + v.bytesDownloaded
238
+ end
239
+ @usageTracker.update(usage)
240
+ rescue
241
+ puts "Error in usage tracking thread: #{$!}"
242
+ puts $!.backtrace.join "\n"
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,335 @@
1
+ require 'quartz_flow/model'
2
+
3
+ class Bucket
4
+ def initialize(label = nil, criteriaData = nil, value = nil)
5
+ @label = label
6
+ @criteriaData = criteriaData
7
+ @value = value
8
+ @absoluteUsageAtStartOfBucket = nil
9
+ end
10
+ attr_accessor :label
11
+ # Data used by BucketChangeCriteria to determine when we need a new bucket.
12
+ attr_accessor :criteriaData
13
+ # At the time this bucket was created, the value of the absolute usage for all time
14
+ attr_accessor :absoluteUsageAtStartOfBucket
15
+ # The amount of usage for this bucket alone
16
+ attr_accessor :value
17
+
18
+ def toHash
19
+ {"label" => @label, "absoluteUsageAtStartOfBucket" => @absoluteUsageAtStartOfBucket, "criteriaData" => @criteriaData, "value" => @value}
20
+ end
21
+
22
+ def fromHash(hash)
23
+ @label = hash["label"]
24
+ @absoluteUsageAtStartOfBucket = hash["absoluteUsageAtStartOfBucket"]
25
+ @criteriaData = hash["criteriaData"]
26
+ @value = hash["value"]
27
+ end
28
+
29
+ def fromModel(bucket)
30
+ @label = bucket.label
31
+ @absoluteUsageAtStartOfBucket = bucket.absoluteUsage
32
+ @criteriaData = bucket.criteriaData
33
+ @value = bucket.value
34
+ end
35
+
36
+ def toModel
37
+ model = UsageBucket.new
38
+ model.attributes = {
39
+ :label => @label,
40
+ :absoluteUsage => @absoluteUsageAtStartOfBucket,
41
+ :criteriaData => @criteriaData,
42
+ :value => @value
43
+ }
44
+ model
45
+ end
46
+ end
47
+
48
+ class BucketChangeCriteria
49
+ # Is it now time for a new bucket?
50
+ def newBucket?(currentBucket)
51
+ false
52
+ end
53
+
54
+ # Make a new bucket and return it.
55
+ def newBucket
56
+ innerNewBucket
57
+ end
58
+
59
+ def innerNewBucket
60
+ raise "Implement me!"
61
+ end
62
+ end
63
+
64
+ class PeriodicBuckets
65
+ def initialize(criteria, maxBuckets = nil)
66
+ setBucketChangeCriteria(criteria)
67
+ @buckets = []
68
+ @maxBuckets = maxBuckets
69
+ @maxBuckets = 1 if @maxBuckets && @maxBuckets < 1
70
+ end
71
+
72
+ # Set the criteria that determines when the current bucket is full
73
+ # and we should make a new empty bucket the current bucket, and that is used
74
+ # to set the label for the new bucket.
75
+ def setBucketChangeCriteria(criteria)
76
+ @bucketChangeCriteria = criteria
77
+ end
78
+
79
+ def update(absoluteUsage = nil)
80
+ if @buckets.size == 0
81
+ prev = nil
82
+ @buckets.push @bucketChangeCriteria.newBucket
83
+ setAbsoluteUsage(prev, @buckets.last, absoluteUsage) if absoluteUsage
84
+ else
85
+ prev = @buckets.last
86
+ # Time for a new bucket?
87
+ if @bucketChangeCriteria.newBucket?(@buckets.last)
88
+ @buckets.push @bucketChangeCriteria.newBucket
89
+ setAbsoluteUsage(prev, @buckets.last, absoluteUsage) if absoluteUsage
90
+ end
91
+ @buckets.shift if @maxBuckets && @buckets.size > @maxBuckets
92
+ end
93
+
94
+ setValue(@buckets.last, absoluteUsage) if absoluteUsage
95
+ end
96
+
97
+ def current(absoluteUsage = nil)
98
+ @buckets.last
99
+ end
100
+
101
+ def all
102
+ @buckets
103
+ end
104
+
105
+ def toHash
106
+ array = []
107
+ @buckets.each do |b|
108
+ array.push b.toHash
109
+ end
110
+ { "buckets" => array }
111
+ end
112
+
113
+ def fromHash(hash)
114
+ @buckets = []
115
+ hash["buckets"].each do |b|
116
+ bucket = Bucket.new(nil, nil, nil)
117
+ bucket.fromHash b
118
+ @buckets.push bucket
119
+ end
120
+ end
121
+
122
+ def fromModel(type)
123
+ @buckets = []
124
+ buckets = UsageBucket.all(:type => type, :order => [:index.asc])
125
+ buckets.each do |model|
126
+ bucket = Bucket.new(nil, nil, nil)
127
+ bucket.fromModel model
128
+ @buckets.push bucket
129
+ end
130
+ end
131
+
132
+ def toModel(type)
133
+ UsageBucket.all(:type => type).destroy!
134
+ index = 0
135
+ @buckets.each do |b|
136
+ model = b.toModel
137
+ model.type = type
138
+ model.index = index
139
+ index += 1
140
+ model.save
141
+ end
142
+ end
143
+
144
+ private
145
+ def setAbsoluteUsage(previousBucket, newBucket, absoluteUsage)
146
+ if previousBucket
147
+ newBucket.absoluteUsageAtStartOfBucket = previousBucket.absoluteUsageAtStartOfBucket + previousBucket.value
148
+ else
149
+ newBucket.absoluteUsageAtStartOfBucket = absoluteUsage
150
+ end
151
+ end
152
+ def setValue(newBucket, absoluteUsage)
153
+ newBucket.value =
154
+ absoluteUsage -
155
+ newBucket.absoluteUsageAtStartOfBucket
156
+ end
157
+ end
158
+
159
+ class DailyBucketChangeCriteria < BucketChangeCriteria
160
+ def newBucket?(currentBucket)
161
+ now = Time.new
162
+ currentBucket.criteriaData.day != now.day
163
+ end
164
+
165
+ def newBucket
166
+ now = Time.new
167
+ Bucket.new(now.strftime("%b %e"), now, 0)
168
+ end
169
+
170
+ def criteriaData
171
+ Time.new
172
+ end
173
+ end
174
+
175
+ class MonthlyBucketChangeCriteria < BucketChangeCriteria
176
+ def initialize(resetDay)
177
+ @resetDay = resetDay
178
+ end
179
+
180
+ def newBucket?(currentBucket)
181
+ Time.new > currentBucket.criteriaData
182
+ end
183
+
184
+ def newBucket
185
+ now = Time.new
186
+ # Set the bucket's criteriaData to the date after which we need a new bucket.
187
+ data = criteriaData
188
+ Bucket.new(now.strftime("%b %Y"), data, 0)
189
+ end
190
+
191
+ def criteriaData
192
+ now = Time.new
193
+ nextMonth = now.mon % 12 + 1
194
+ Time.local(now.year, nextMonth, @resetDay)
195
+ end
196
+ end
197
+
198
+
199
+ # For testing
200
+ class MinuteBucketChangeCriteria < BucketChangeCriteria
201
+ def newBucket?(currentBucket)
202
+ now = Time.new
203
+ currentBucket.criteriaData.min != now.min
204
+ end
205
+
206
+ def newBucket
207
+ now = Time.new
208
+ Bucket.new(now.strftime("%H:%M"), now, 0)
209
+ end
210
+ end
211
+
212
+ class UsageTracker
213
+ def initialize(monthlyResetDay)
214
+ @buckets = {}
215
+ @buckets[:daily] = PeriodicBuckets.new(DailyBucketChangeCriteria.new,31)
216
+ #@buckets[:minute] = PeriodicBuckets.new(MinuteBucketChangeCriteria.new,3)
217
+ @buckets[:monthly] = PeriodicBuckets.new(MonthlyBucketChangeCriteria.new(monthlyResetDay),2)
218
+ @usageForAllTimeAdjustment = 0
219
+ loadBucketsFromDatastore
220
+ end
221
+
222
+ # Update the UsageTracker with more usage. The value passed
223
+ # should be the usage since the torrentflow session was created.
224
+ # If a datastore is not used, then this means stopping and starting the session
225
+ # will cause UsageTracking to only track usage for the session. However
226
+ # if Mongo is used then the usage can be saved and persisted between sessions
227
+ # and internally the value passed here is added to the value loaded from Mongo.
228
+ def update(usageForAllTime)
229
+ usageForAllTime += @usageForAllTimeAdjustment
230
+ @buckets.each do |k,buckets|
231
+ buckets.update usageForAllTime
232
+ end
233
+ saveBucketsToDatastore
234
+ end
235
+
236
+ # This method returns the usage in the current bucket for the specified
237
+ # period type (:daily or :monthly). The usage is accurate as of the last
238
+ # time update() was called.
239
+ # The returned value is a single Bucket object.
240
+ def currentUsage(periodType)
241
+ getBuckets(periodType).current
242
+ end
243
+
244
+ # Returns the usage as of the last time update() was called.
245
+ # This method returns all the tracked usage for the specified
246
+ # period type (:daily or :monthly). The usage is accurate as of the last
247
+ # time update() was called.
248
+ # The returned value is an array of Bucket objects.
249
+ def allUsage(periodType)
250
+ getBuckets(periodType).all
251
+ end
252
+
253
+ private
254
+ def getBuckets(type)
255
+ buckets = @buckets[type]
256
+ raise "Unsupported periodType #{periodType.to_s}" if ! buckets
257
+ buckets
258
+ end
259
+
260
+ def saveBucketsToDatastore
261
+ @buckets[:daily].toModel(:daily)
262
+ @buckets[:monthly].toModel(:monthly)
263
+ =begin
264
+ if @mongoDb
265
+ dailyCollection = @mongoDb.collection("daily_usage")
266
+ monthlyCollection = @mongoDb.collection("monthly_usage")
267
+ # Remove all previous documents
268
+ dailyCollection.remove
269
+ monthlyCollection.remove
270
+
271
+ dailyCollection.insert @buckets[:daily].toHash
272
+ monthlyCollection.insert @buckets[:monthly].toHash
273
+ end
274
+ =end
275
+ end
276
+
277
+ def loadBucketsFromDatastore
278
+ @buckets[:daily].fromModel(:daily)
279
+ @buckets[:monthly].fromModel(:monthly)
280
+
281
+ # If we are loading from datastore it means that the absolute usage returned from the current torrent session will not
282
+ # contain the usage that we previously tracked, so we must add the old tracked value to what the torrent
283
+ # session reports.
284
+ if @buckets[:daily].current
285
+ @usageForAllTimeAdjustment = @buckets[:daily].current.absoluteUsageAtStartOfBucket + @buckets[:daily].current.value
286
+ end
287
+
288
+ =begin
289
+ if @mongoDb
290
+ $logger.info "Loading usage from Mongo."
291
+ dailyCollection = @mongoDb.collection("daily_usage")
292
+ monthlyCollection = @mongoDb.collection("monthly_usage")
293
+
294
+ arr = dailyCollection.find_one
295
+ @buckets[:daily].fromHash arr if arr
296
+ arr = monthlyCollection.find_one
297
+ @buckets[:monthly].fromHash arr if arr
298
+
299
+ # If we are loading from Mongo it means that the absolute usage returned from the torrentflow session will not
300
+ # contain the usage that we previously tracked, so we must add the old tracked value to what the torrentflow
301
+ # session reports.
302
+ if @buckets[:daily].current
303
+ @usageForAllTimeAdjustment = @buckets[:daily].current.absoluteUsageAtStartOfBucket + @buckets[:daily].current.value
304
+ $logger.info "Absolute usage at start of current daily bucket: " + @buckets[:daily].current.absoluteUsageAtStartOfBucket.to_s
305
+ $logger.info "Usage in current daily bucket: " + @buckets[:daily].current.value.to_s
306
+ $logger.info "Usage for all time adjustment: " + @usageForAllTimeAdjustment.to_s
307
+ else
308
+ $logger.info "No usage loaded in Mongo (empty collection)."
309
+ end
310
+ else
311
+ $logger.info "Not loading usage from Mongo."
312
+ end
313
+ =end
314
+ end
315
+ end
316
+
317
+ =begin
318
+ # Testing
319
+ tracker = UsageTracker.new
320
+
321
+ abs = 200
322
+ while true
323
+ tracker.update(abs)
324
+
325
+ puts
326
+ puts "Usage for all time: #{abs}"
327
+ puts "Buckets:"
328
+ tracker.allUsage(:minute).each do |b|
329
+ puts " #{b.label}: #{b.value}"
330
+ end
331
+ abs += 10
332
+ sleep 10
333
+ end
334
+
335
+ =end
@@ -0,0 +1,124 @@
1
+ require 'quartz_torrent/formatter.rb'
2
+
3
+ module QuartzTorrent
4
+
5
+ class Metainfo::FileInfo
6
+ def to_h
7
+ { path: @path, length: @length }
8
+ end
9
+ end
10
+
11
+ class Metainfo::Info
12
+ def to_h
13
+ result = {}
14
+
15
+ result[:name] = @name
16
+ result[:pieceLen] = @pieceLen
17
+ result[:files] = @files.collect{ |e| e.to_h }
18
+
19
+ result
20
+ end
21
+ end
22
+
23
+ class TrackerPeer
24
+ def to_h
25
+ { ip: @ip, port: @port }
26
+ end
27
+ end
28
+
29
+ class Peer
30
+ def to_h
31
+ result = {}
32
+
33
+ result[:trackerPeer] = @trackerPeer.to_h
34
+ result[:amChoked] = @amChoked
35
+ result[:amInterested] = @amInterested
36
+ result[:peerChoked] = @peerChoked
37
+ result[:peerInterested] = @peerInterested
38
+ result[:firstEstablishTime] = @firstEstablishTime
39
+ result[:maxRequestedBlocks] = @maxRequestedBlocks
40
+ result[:state] = @state
41
+ result[:isUs] = @isUs
42
+ result[:uploadRate] = @uploadRate
43
+ result[:downloadRate] = @downloadRate
44
+ if @bitfield
45
+ result[:pctComplete] = "%.2f" % (100.0 * @bitfield.countSet / @bitfield.length)
46
+ else
47
+ result[:pctComplete] = 0
48
+ end
49
+
50
+ result
51
+ end
52
+ end
53
+
54
+ class TorrentDataDelegate
55
+ # Convert to a hash. Also flattens some of the data into new fields.
56
+ def to_h
57
+ result = {}
58
+
59
+ ## Extra fields added by this method:
60
+ # Length of the torrent
61
+ result[:dataLength] = @info ? @info.dataLength : 0
62
+ # Percent complete
63
+ pct = withCurrentAndTotalBytes{ |cur, total| (cur.to_f / total.to_f * 100.0).round 1 }
64
+ result[:percentComplete] = pct
65
+ # Time left
66
+ secondsLeft = withCurrentAndTotalBytes do |cur, total|
67
+ if @downloadRateDataOnly && @downloadRateDataOnly > 0
68
+ (total.to_f - cur.to_f) / @downloadRateDataOnly
69
+ else
70
+ 0
71
+ end
72
+ end
73
+ result[:timeLeft] = Formatter.formatTime(secondsLeft)
74
+
75
+ ## Regular fields
76
+ result[:info] = @info ? @info.to_h : nil
77
+ result[:infoHash] = @infoHash
78
+ result[:recommendedName] = @recommendedName
79
+ result[:downloadRate] = @downloadRate
80
+ result[:uploadRate] = @uploadRate
81
+ result[:downloadRateDataOnly] = @downloadRateDataOnly
82
+ result[:uploadRateDataOnly] = @uploadRateDataOnly
83
+ result[:completedBytes] = @completedBytes
84
+ result[:peers] = @peers.collect{ |p| p.to_h }
85
+ result[:state] = @state
86
+ #result[:completePieceBitfield] = @completePieceBitfield
87
+ result[:metainfoLength] = @metainfoLength
88
+ result[:metainfoCompletedLength] = @metainfoCompletedLength
89
+ result[:paused] = @paused
90
+ result[:uploadRateLimit] = @uploadRateLimit
91
+ result[:downloadRateLimit] = @downloadRateLimit
92
+ result[:ratio] = @ratio
93
+ result[:bytesUploaded] = @bytesUploaded
94
+ result[:bytesDownloaded] = @bytesDownloaded
95
+
96
+ result
97
+ end
98
+
99
+ private
100
+ def withCurrentAndTotalBytes
101
+ if @info
102
+ yield @completedBytes, @info.dataLength
103
+ elsif @state == :downloading_metainfo && @metainfoCompletedLength && @metainfoLength
104
+ yield @metainfoCompletedLength, @metainfoLength
105
+ else
106
+ 0
107
+ end
108
+ end
109
+
110
+
111
+ def calcEstimatedTime(torrent)
112
+ # Time left = amount_left / download_rate
113
+ # = total_size * (1-progress) / download_rate
114
+ if torrentHandle.has_metadata && torrentHandle.status.download_rate.to_f > 0
115
+ secondsLeft = torrentHandle.info.total_size.to_f * (1 - torrentHandle.status.progress.to_f) / torrentHandle.status.download_rate.to_f
116
+ Formatter.formatTime(secondsLeft)
117
+ else
118
+ "unknown"
119
+ end
120
+ end
121
+
122
+ end
123
+ end
124
+