rubytorrent 0.3

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,186 @@
1
+ ## util.rb -- miscellaneous RubyTorrent utility modules.
2
+ ## Copyright 2005 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ def rt_debug(*args)
15
+ if $DEBUG || RubyTorrent.log
16
+ stream = RubyTorrent.log || $stdout
17
+ stream << args.join << "\n"
18
+ stream.flush
19
+ end
20
+ end
21
+
22
+ def rt_warning(*args)
23
+ if $DEBUG || RubyTorrent.log
24
+ stream = RubyTorrent.log || $stderr
25
+ stream << "warning: " << args.join << "\n"
26
+ stream.flush
27
+ end
28
+ end
29
+
30
+ module RubyTorrent
31
+
32
+ @log = nil
33
+ def log_output_to(fn)
34
+ @log = File.open(fn, "w")
35
+ end
36
+ attr_reader :log
37
+ module_function :log_output_to, :log
38
+
39
+
40
+ ## parse final hash of pseudo-keyword arguments
41
+ def get_args(rest, *names)
42
+ hash = rest.find { |x| x.is_a? Hash }
43
+ if hash
44
+ rest.delete hash
45
+ hash.each { |k, v| raise ArgumentError, %{unknown argument "#{k}"} unless names.include?(k) }
46
+ end
47
+
48
+ [hash || {}, rest]
49
+ end
50
+ module_function :get_args
51
+
52
+ ## "events": very similar to Observable, but cleaner, IMO. events are
53
+ ## listened to and sent in instance space, but registered in class
54
+ ## space. example:
55
+ ##
56
+ ## class C
57
+ ## include EventSource
58
+ ## event :goat, :boat
59
+ ##
60
+ ## def send_events
61
+ ## send_event :goat
62
+ ## send_event(:boat, 3)
63
+ ## end
64
+ ## end
65
+ ##
66
+ ## c = C.new
67
+ ## c.on_event(:goat) { puts "got goat!" }
68
+ ## c.on_event(:boat) { |x| puts "got boat: #{x}" }
69
+ ##
70
+ ## Defining them in class space is not really necessary, except as an
71
+ ## error-checking mechanism.
72
+ module EventSource
73
+ def on_event(who, *events, &b)
74
+ @event_handlers ||= Hash.new { [] }
75
+ events.each do |e|
76
+ raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
77
+ @event_handlers[e] <<= [who, b]
78
+ end
79
+ nil
80
+ end
81
+
82
+ def send_event(e, *args)
83
+ raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
84
+ @event_handlers ||= Hash.new { [] }
85
+ @event_handlers[e].each { |who, proc| proc[self, *args] }
86
+ nil
87
+ end
88
+
89
+ def unregister_events(who, *events)
90
+ @event_handlers.each do |event, handlers|
91
+ handlers.each do |ewho, proc|
92
+ if (ewho == who) && (events.empty? || events.member?(event))
93
+ @event_handlers[event].delete [who, proc]
94
+ end
95
+ end
96
+ end
97
+ nil
98
+ end
99
+
100
+ def relay_event(who, *events)
101
+ @event_handlers ||= Hash.new { [] }
102
+ events.each do |e|
103
+ raise "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
104
+ raise "unknown event #{e} for #{who.class}" unless (who.class.class_eval "@@event_has")[e]
105
+ @event_handlers[e] <<= [who, lambda { |s, *a| who.send_event e, *a }]
106
+ end
107
+ nil
108
+ end
109
+
110
+ def self.append_features(mod)
111
+ super(mod)
112
+ mod.class_eval %q{
113
+ @@event_has ||= Hash.new(false)
114
+ def self.event(*args)
115
+ args.each { |a| @@event_has[a] = true }
116
+ end
117
+ }
118
+ end
119
+ end
120
+
121
+ ## ensure that a method doesn't execute more frequently than some
122
+ ## number of seconds. e.g.:
123
+ ##
124
+ ## def meth
125
+ ## ...
126
+ ## end
127
+ ## min_iterval :meth, 10
128
+ ##
129
+ ## ensures that "meth" won't be executed more than once every 10
130
+ ## seconds.
131
+ module MinIntervalMethods
132
+ def min_interval(meth, int)
133
+ class_eval %{
134
+ @@min_interval ||= {}
135
+ @@min_interval[:#{meth}] = [nil, #{int.to_i}]
136
+ alias :min_interval_#{meth} :#{meth}
137
+ def #{meth}(*a, &b)
138
+ last, int = @@min_interval[:#{meth}]
139
+ unless last && ((Time.now - last) < int)
140
+ min_interval_#{meth}(*a, &b)
141
+ @@min_interval[:#{meth}][0] = Time.now
142
+ end
143
+ end
144
+ }
145
+ end
146
+ end
147
+
148
+ ## boolean attributes now get question marks in their accessors
149
+ ## don't forget to 'extend' rather than 'include' this one
150
+ module AttrReaderQ
151
+ def attr_reader_q(*args)
152
+ args.each { |v| class_eval "def #{v}?; @#{v}; end" }
153
+ end
154
+
155
+ def attr_writer_q(*args)
156
+ args.each { |v| attr_writer v }
157
+ end
158
+
159
+ def attr_accessor_q(*args)
160
+ attr_reader_q args
161
+ attr_writer_q args
162
+ end
163
+ end
164
+
165
+ module ArrayShuffle
166
+ def shuffle!
167
+ each_index do |i|
168
+ j = i + rand(self.size - i);
169
+ self[i], self[j] = self[j], self[i]
170
+ end
171
+ end
172
+
173
+ def shuffle
174
+ self.clone.shuffle! # dup doesn't preserve shuffle! method
175
+ end
176
+ end
177
+
178
+ module StringMapBytes
179
+ def map_bytes
180
+ ret = []
181
+ each_byte { |x| ret.push(yield(x)) }
182
+ ret
183
+ end
184
+ end
185
+
186
+ end
@@ -0,0 +1,211 @@
1
+ ## make-metainfo.rb -- interactive .torrent creater
2
+ ## Copyright 2005 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require 'digest/sha1'
15
+ require "rubytorrent"
16
+
17
+ def die(x); $stderr << "#{x}\n" && exit(-1); end
18
+ def syntax
19
+ %{
20
+ Syntax: make-metainfo.rb [<file or directory>]+
21
+
22
+ make-metainfo is an interactive program for creating .torrent files from a set
23
+ of files or directories. any directories specified will be scanned recursively.
24
+ }
25
+ end
26
+
27
+ def find_files(f)
28
+ if FileTest.directory? f
29
+ Dir.new(f).entries.map { |x| find_files(File.join(f, x)) unless x =~ /^\.[\.\/]*$/}.compact
30
+ else
31
+ f
32
+ end
33
+ end
34
+
35
+ class Numeric
36
+ def to_size_s
37
+ if self < 1024
38
+ "#{self.round}b"
39
+ elsif self < 1024**2
40
+ "#{(self / 1024.0).round}kb"
41
+ elsif self < 1024**3
42
+ "#{(self / (1024.0**2)).round}mb"
43
+ else
44
+ "#{(self / (1024.0**3)).round}gb"
45
+ end
46
+ end
47
+ end
48
+
49
+ def read_pieces(files, length)
50
+ buf = ""
51
+ files.each do |f|
52
+ File.open(f) do |fh|
53
+ begin
54
+ read = fh.read(length - buf.length)
55
+ if (buf.length + read.length) == length
56
+ yield(buf + read)
57
+ buf = ""
58
+ else
59
+ buf += read
60
+ end
61
+ end until fh.eof?
62
+ end
63
+ end
64
+
65
+ yield buf
66
+ end
67
+
68
+ die syntax if ARGV.length == 0
69
+
70
+ puts "Scanning..."
71
+ files = ARGV.map { |f| find_files f }.flatten
72
+ single = files.length == 1
73
+
74
+ puts "Building #{(single ? 'single' : 'multi')}-file .torrent for #{files.length} file#{(single ? '' : 's')}."
75
+
76
+ mi = RubyTorrent::MetaInfo.new
77
+ mii = RubyTorrent::MetaInfoInfo.new
78
+
79
+ maybe_name = if single
80
+ ARGV[0]
81
+ else
82
+ (File.directory?(ARGV[0]) ? File.basename(ARGV[0]) : File.basename(File.dirname(ARGV[0])))
83
+ end
84
+ puts
85
+ print %{Default output file/directory name (enter for "#{maybe_name}"): }
86
+ name = $stdin.gets.chomp
87
+ mii.name = (name == "" ? maybe_name : name)
88
+ puts %{We'll use "#{mii.name}".}
89
+
90
+ puts
91
+ puts "Measuring..."
92
+ length = nil
93
+ if single
94
+ length = mii.length = files.inject(0) { |s, f| s + File.size(f) }
95
+ else
96
+ mii.files = []
97
+ length = files.inject(0) do |s, f|
98
+ miif = RubyTorrent::MetaInfoInfoFile.new
99
+ miif.length = File.size f
100
+ miif.path = f.split File::SEPARATOR
101
+ miif.path = miif.path[1, miif.path.length - 1] if miif.path[0] == mii.name
102
+ mii.files << miif
103
+ s + miif.length
104
+ end
105
+ end
106
+
107
+ puts <<EOS
108
+
109
+ The file is #{length.to_size_s}. What piece size would you like? A smaller piece size
110
+ will result in a larger .torrent file; a larger piece size may cause
111
+ transfer inefficiency. Common sizes are 256, 512, and 1024kb.
112
+
113
+ Hint: for this .torrent,
114
+ EOS
115
+
116
+ size = nil
117
+ [64, 128, 256, 512, 1024, 2048, 4096].each do |size|
118
+ num_pieces = (length.to_f / size / 1024.0).ceil
119
+ tsize = num_pieces.to_f * 20.0 + 100
120
+ puts " - piece size of #{size}kb => #{num_pieces} pieces and .torrent size of approx. #{tsize.to_size_s}."
121
+ break if tsize < 10240
122
+ end
123
+
124
+ maybe_plen = [size, 256].min
125
+ begin
126
+ print "Piece size in kb (enter for #{maybe_plen}k): "
127
+ plen = $stdin.gets.chomp
128
+ end while plen !~ /^\d*$/
129
+
130
+ plen = (plen == "" ? maybe_plen : plen.to_i)
131
+
132
+ mii.piece_length = plen * 1024
133
+ num_pieces = (length.to_f / mii.piece_length.to_f).ceil
134
+ puts "Using piece size of #{plen}kb => .torrent size of approx. #{(num_pieces * 20.0).to_size_s}."
135
+
136
+ print "Calculating #{num_pieces} piece SHA1s... " ; $stdout.flush
137
+
138
+ mii.pieces = ""
139
+ i = 0
140
+ read_pieces(files, mii.piece_length) do |piece|
141
+ mii.pieces += Digest::SHA1.digest(piece)
142
+ i += 1
143
+ if (i % 100) == 0
144
+ print "#{(i.to_f / num_pieces * 100.0).round}%... "; $stdout.flush
145
+ end
146
+ end
147
+ puts "done"
148
+
149
+ mi.info = mii
150
+ puts <<EOS
151
+
152
+ Enter the tracker URL or URLs that will be hosting the .torrent
153
+ file. These are typically of the form:
154
+
155
+ http://tracker.example.com:6969/announce
156
+
157
+ Multiple trackers may be partitioned into tiers; clients will try all
158
+ servers (in random order) from an earlier tier before trying those of
159
+ a later tier. See http://home.elp.rr.com/tur/multitracker-spec.txt
160
+ for details.
161
+
162
+ Enter the tracker URL(s) now. Separate multiple tracker URLs on the
163
+ same tier with spaces. Enter a blank line when you're done.
164
+
165
+ (Note that if you have multiple trackers, some clients may only use
166
+ the first one, so that should be the one capable of handling the most
167
+ traffic.)
168
+ EOS
169
+
170
+ tier = 0
171
+ trackers = []
172
+ begin
173
+ print "Tier #{tier} tracker(s): "
174
+ these = $stdin.gets.chomp.split(/\s+/)
175
+ trackers.push these unless these.length == 0
176
+ tier += 1 unless these.length == 0
177
+ end while (these.length != 0) || (tier == 0)
178
+
179
+ mi.announce = URI.parse(trackers[0][0])
180
+ mi.announce_list = trackers.map do |tier|
181
+ tier.map { |x| URI.parse(x) }
182
+ end unless (trackers.length == 1) && (trackers[0].length == 1)
183
+
184
+ puts <<EOS
185
+
186
+ Enter any comments. No one will probably ever see these. End with a blank line.
187
+ EOS
188
+ comm = ""
189
+ while true
190
+ s = $stdin.gets.chomp
191
+ break if s == ""
192
+ comm += s + "\n"
193
+ end
194
+ mi.comment = comm.chomp unless comm == ""
195
+
196
+ mi.created_by = "RubyTorrent make-metainfo (http://rubytorrent.rubyforge.org)"
197
+ mi.creation_date = Time.now
198
+
199
+ maybe_name = "#{mii.name}.torrent"
200
+ begin
201
+ print "Output filename (enter for #{maybe_name}): "
202
+ name = $stdin.gets.chomp
203
+ end while name.length == ""
204
+
205
+ name = (name == "" ? maybe_name : name)
206
+ File.open(name, "w") do |f|
207
+ f.write mi.to_bencoding
208
+ end
209
+
210
+ puts "Succesfully created #{name}"
211
+
@@ -0,0 +1,340 @@
1
+ ## rtpeer-ncurses.rb -- RubyTorrent ncurses BitTorrent peer.
2
+ ## Copyright 2005 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require "rubygems"
15
+ require "rubytorrent"
16
+ require "ncurses"
17
+ require "optparse"
18
+
19
+ def die(x); $stderr << "#{x}\n" && exit(-1); end
20
+
21
+ dlratelim = nil
22
+ ulratelim = nil
23
+
24
+ opts = OptionParser.new do |opts|
25
+ opts.banner =
26
+ %{Usage: rtpeer-ncurses [options] <torrent> [<target>]
27
+
28
+ rtpeer-ncurses is a very simple ncurses-based BitTorrent peer. You can use it
29
+ to download .torrents or to seed them.
30
+
31
+ <torrent> is a .torrent filename or URL.
32
+ <target> is a file or directory on disk. If not specified, the default value
33
+ from <torrent> will be used.
34
+ [options] are:
35
+ }
36
+
37
+ opts.on("-l", "--log FILENAME",
38
+ "Log events to FILENAME (for debugging)") do |fn|
39
+ RubyTorrent::log_output_to(fn)
40
+ end
41
+
42
+ opts.on("-d", "--downlimit LIMIT", Integer,
43
+ "Limit download rate to LIMIT kb/s") do |x|
44
+ dlratelim = x * 1024
45
+ end
46
+
47
+ opts.on("-u", "--uplimit LIMIT", Integer,
48
+ "Limit upload rate to LIMIT kb/s") do |x|
49
+ ulratelim = x * 1024
50
+ end
51
+
52
+ opts.on_tail("-h", "--help", "Show this message") do
53
+ puts opts
54
+ exit
55
+ end
56
+ end
57
+
58
+ opts.parse!(ARGV)
59
+ proxy = ENV["http_proxy"]
60
+ torrent = ARGV.shift or (puts opts; exit)
61
+ dest = ARGV.shift
62
+
63
+ class Numeric
64
+ def to_sz
65
+ if self < 1024
66
+ "#{self.round}b"
67
+ elsif self < 1024 ** 2
68
+ "#{(self / 1024 ).round}k"
69
+ elsif self < 1024 ** 3
70
+ sprintf("%.1fm", self.to_f / (1024 ** 2))
71
+ else
72
+ sprintf("%.2fg", self.to_f / (1024 ** 3))
73
+ end
74
+ end
75
+
76
+ MIN = 60
77
+ HOUR = 60 * MIN
78
+ DAY = 24 * HOUR
79
+
80
+ def to_time
81
+ if self < MIN
82
+ sprintf("0:%02d", self)
83
+ elsif self < HOUR
84
+ sprintf("%d:%02d", self / MIN, self % MIN)
85
+ elsif self < DAY
86
+ sprintf("%d:%02d:%02d", self / HOUR, (self % HOUR) / MIN, (self % HOUR) % MIN)
87
+ else
88
+ sprintf("%dd %d:%02d:%02d", self / DAY, (self % DAY) / HOUR, ((self % DAY) % HOUR) / MIN, ((self % DAY) % HOUR) % MIN)
89
+ end
90
+ end
91
+ end
92
+
93
+ class NilClass
94
+ def to_time; "--:--"; end
95
+ def to_sz; "-"; end
96
+ end
97
+
98
+ class Display
99
+ STALL_SECS = 15
100
+
101
+ attr_accessor :fn, :dest, :status,:dlamt, :ulamt, :dlrate, :ulrate,
102
+ :conn_peers, :fail_peers, :untried_peers, :tracker,
103
+ :errcount, :completed, :total, :rate, :use_rate
104
+
105
+ def initialize(window)
106
+ @window = window
107
+ @need_update = true
108
+
109
+ @fn = ""
110
+ @dest = ""
111
+ @status = ""
112
+ @completed = 0
113
+ @total = 0
114
+ @dlamt = 0
115
+ @dlrate = 0
116
+ @ulamt = 0
117
+ @ulrate = 0
118
+ @rate = 0
119
+ @conn_peers = 0
120
+ @fail_peers = 0
121
+ @untried_peers = 0
122
+ @tracker = "not connected"
123
+ @errcount = 0
124
+ @dlblocks = 0
125
+ @ulblocks = 0
126
+
127
+ @got_blocks = 0
128
+ @sent_blocks = 0
129
+ @last_got_block = nil
130
+ @last_sent_block = nil
131
+ @start_time = nil
132
+
133
+ @use_rate = false
134
+ end
135
+
136
+ def got_block
137
+ @got_blocks += 1
138
+ @last_got_block = Time.now
139
+ end
140
+
141
+ def sent_block
142
+ @sent_blocks += 1
143
+ @last_sent_block = Time.now
144
+ end
145
+
146
+ def sigwinch_handler(sig = nil)
147
+ @need_update = true
148
+ end
149
+
150
+ def start_timer
151
+ @start_time = Time.now
152
+ end
153
+
154
+ def draw
155
+ if @need_update
156
+ update_size
157
+ @window.erase
158
+ end
159
+
160
+ complete_width = [@cols - 23, 0].max
161
+ complete_ticks = ((@completed.to_f / @total) * complete_width)
162
+
163
+ elapsed = (@start_time ? Time.now - @start_time : nil)
164
+ rate = (use_rate ? @rate : @dlrate)
165
+ remaining = rate && (rate > 0 ? (@total - @completed).to_f / rate : nil)
166
+
167
+ dlstall = @last_got_block && ((Time.now - @last_got_block) > STALL_SECS)
168
+ ulstall = @last_sent_block && ((Time.now - @last_sent_block) > STALL_SECS)
169
+
170
+ line = 1
171
+ [
172
+ "Contents: #@fn",
173
+ " Dest: #@dest",
174
+ "",
175
+ " Status: #@status",
176
+ "Progress: [" + ("#" * complete_ticks),
177
+ " Time: elapsed #{elapsed.to_time}, remaining #{remaining.to_time}",
178
+ "Download: #{@dlamt.to_sz} at #{dlstall ? '(stalled)' : @dlrate.to_sz + '/s'}",
179
+ " Upload: #{@ulamt.to_sz} at #{ulstall ? '(stalled)' : @ulrate.to_sz + '/s'}",
180
+ " Peers: connected to #@conn_peers (#@fail_peers failed, #@untried_peers untried)",
181
+ " Tracker: #@tracker",
182
+ " Errors: #@errcount",
183
+ ].each do |s|
184
+ break if line > @rows
185
+ @window.mvaddnstr(line, 2, s + (" " * @cols), @cols - 4)
186
+ line += 1
187
+ end
188
+
189
+ ## progress bar tail
190
+ @window.mvaddstr(5, @cols - 11, sprintf("] %.2f%% ", (@completed.to_f / @total) * 100.0))
191
+ @window.mvaddnstr(7, 31, "|" + ("#" * (@dlrate / 1024)) + (" " * @cols), @cols - 31 - 2)
192
+ @window.mvaddnstr(8, 31, "|" + ('#' * (@ulrate / 1024)) + (" " * @cols), @cols - 31 - 2)
193
+
194
+ @window.box(0,0)
195
+
196
+ # @got_blocks -= 1 unless @got_blocks == 0
197
+ # @sent_blocks -= 1 unless @sent_blocks == 0
198
+ end
199
+
200
+ private
201
+
202
+ def update_size
203
+ rows = []
204
+ cols = []
205
+ ## jesus CHRIST this is a shitty interface.
206
+ @window.getmaxyx(rows, cols)
207
+ @rows = rows[0]
208
+ @cols = cols[0]
209
+ @need_update = false
210
+ end
211
+ end
212
+
213
+ begin
214
+ mi = RubyTorrent::MetaInfo.from_location(torrent, proxy)
215
+ rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
216
+ die %{Error: can\'t parse metainfo file "#{torrent}"---maybe not a .torrent?}
217
+ rescue RubyTorrent::TypedStructError => e
218
+ $stderr << <<EOS
219
+ error parsing metainfo file, and it's likely something I should know about.
220
+ please email the torrent file to wmorgan-rubytorrent-bug@masanjin.net,
221
+ along with this backtrace: (this is RubyTorrent version #{RubyTorrent::VERSION})
222
+ EOS
223
+
224
+ raise e
225
+ rescue IOError, SystemCallError => e
226
+ $stderr.puts %{Error: can't read file "#{torrent}": #{e.message}}
227
+ exit
228
+ end
229
+
230
+ unless dest.nil?
231
+ if FileTest.directory?(dest) && mi.info.single?
232
+ dest = File.join(dest, mi.info.name)
233
+ elsif FileTest.file?(dest) && mi.info.multiple?
234
+ die %{Error: .torrent contains multiple files, but "#{dest}" is a single file (must be a directory)}
235
+ end
236
+ end
237
+
238
+ def handle_any_input(display)
239
+ case(Ncurses.getch())
240
+ when ?q, ?Q
241
+ Ncurses.curs_set(1)
242
+ Ncurses.endwin()
243
+ exit
244
+ when Ncurses::KEY_RESIZE
245
+ display.sigwinch_handler
246
+ end
247
+ end
248
+
249
+ Ncurses.initscr
250
+
251
+ begin
252
+ Ncurses.nl()
253
+ Ncurses.noecho()
254
+ Ncurses.curs_set(0)
255
+ Ncurses.stdscr.nodelay(true)
256
+ Ncurses.timeout(0)
257
+
258
+ display = Display.new Ncurses.stdscr
259
+ display.status = "checking file on disk..."
260
+ display.dest = File.expand_path(dest || mi.info.name) + (mi.single? ? "" : "/")
261
+ if mi.single?
262
+ display.fn = "#{mi.info.name} (#{mi.info.length.to_sz} in one file)"
263
+ else
264
+ display.fn = "#{mi.info.name}/ (#{mi.info.total_length.to_sz} in #{mi.info.files.length} files)"
265
+ end
266
+ display.total = mi.info.num_pieces * mi.info.piece_length
267
+ display.completed = 0
268
+ display.draw; Ncurses.refresh
269
+
270
+ display.use_rate = true
271
+ display.start_timer
272
+ num_pieces = 0
273
+ start = Time.now
274
+ every = 10
275
+ package = RubyTorrent::Package.new(mi, dest) do |piece|
276
+ num_pieces += 1
277
+ if (num_pieces % every) == 0
278
+ display.completed = (num_pieces * mi.info.piece_length)
279
+ display.rate = display.completed.to_f / (Time.now - start)
280
+ handle_any_input display
281
+ display.draw; Ncurses.refresh
282
+ end
283
+ end
284
+
285
+ display.status = "starting peer..."
286
+ display.use_rate = false
287
+ display.draw; Ncurses.refresh
288
+ bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy, :dlratelim => dlratelim, :ulratelim => ulratelim)
289
+
290
+ connecting = true
291
+ bt.on_event(self, :received_block) do |s, b, peer|
292
+ display.got_block
293
+ connecting = false
294
+ end
295
+ bt.on_event(self, :sent_block) do |s, b, peer|
296
+ display.sent_block
297
+ connecting = false
298
+ end
299
+ bt.on_event(self, :discarded_piece) { |s, p| display.errcount += 1 }
300
+ bt.on_event(self, :tracker_connected) do |s, url|
301
+ display.tracker = url
302
+ display.untried_peers = bt.num_possible_peers
303
+ end
304
+ bt.on_event(self, :tracker_lost) { |s, url| display.tracker = "can't connect to #{url}" }
305
+ bt.on_event(self, :forgetting_peer) { |s, p| display.fail_peers += 1 }
306
+ bt.on_event(self, :removed_peer, :added_peer) do |s, p|
307
+ if (display.conn_peers = bt.num_active_peers) == 0
308
+ connecting = true
309
+ end
310
+ end
311
+ bt.on_event(self, :added_peer) { |s, p| display.conn_peers += 1 }
312
+ bt.on_event(self, :trying_peer) { |s, p| display.untried_peers -= 1 unless display.untried_peers == 0 }
313
+
314
+ display.total = bt.total_bytes
315
+ display.start_timer
316
+
317
+ while true
318
+ handle_any_input(display)
319
+
320
+ display.status = if bt.complete?
321
+ "seeding (download complete)"
322
+ elsif connecting
323
+ "connecting to peers"
324
+ else
325
+ "downloading"
326
+ end
327
+ display.draw; Ncurses.refresh
328
+
329
+ display.dlamt = bt.dlamt
330
+ display.dlrate = bt.dlrate
331
+ display.ulamt = bt.ulamt
332
+ display.ulrate = bt.ulrate
333
+ display.completed = bt.bytes_completed
334
+ display.draw; Ncurses.refresh
335
+ sleep(0.5)
336
+ end
337
+ ensure
338
+ Ncurses.curs_set(1)
339
+ Ncurses.endwin()
340
+ end