rubytorrent 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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