xget 2.1.5 → 3.0.0
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.
- checksums.yaml +5 -5
- data/xget.rb +757 -0
- metadata +11 -11
- data/bin/xget +0 -710
data/xget.rb
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# xget.rb - Created by George Watson on 2013/05/19
|
|
4
|
+
# https://github.com/takeiteasy/xget
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2013 George Watson, All rights reserved.
|
|
7
|
+
#
|
|
8
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
9
|
+
# are permitted provided that the following conditions are met:
|
|
10
|
+
#
|
|
11
|
+
# Redistributions of source code must retain the above copyright notice, this list
|
|
12
|
+
# of conditions and the following disclaimer.
|
|
13
|
+
# Redistributions in binary form must reproduce the above copyright notice, this
|
|
14
|
+
# list of conditions and the following disclaimer in the documentation and/or other
|
|
15
|
+
# materials provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# Neither the name of the copyright holder nor the names of its contributors may
|
|
18
|
+
# be used to endorse or promote products derived from this software without specific
|
|
19
|
+
# prior written permission.
|
|
20
|
+
#
|
|
21
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
23
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
24
|
+
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
25
|
+
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
26
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
27
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
28
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
29
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
30
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
|
|
32
|
+
require 'socket'
|
|
33
|
+
require 'thread'
|
|
34
|
+
require 'timeout'
|
|
35
|
+
require 'optparse'
|
|
36
|
+
|
|
37
|
+
# Why isn't this enabled by default?
|
|
38
|
+
Thread.abort_on_exception = true
|
|
39
|
+
# Put standard output into syncronised mode
|
|
40
|
+
$stdout.sync = true
|
|
41
|
+
|
|
42
|
+
# Version values
|
|
43
|
+
$ver_maj, $ver_min, $ver_rev = 2, 2, 1
|
|
44
|
+
$ver_str = "#{$ver_maj}.#{$ver_min}.#{$ver_rev}"
|
|
45
|
+
|
|
46
|
+
config = {
|
|
47
|
+
"out-dir" => './',
|
|
48
|
+
"skip-existing" => false,
|
|
49
|
+
"servers" => {},
|
|
50
|
+
"sleep-interval" => 5,
|
|
51
|
+
"allow-queueing" => false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def puts_error msg
|
|
55
|
+
puts "! \e[31mERROR\e[0m: #{msg}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def puts_abort msg
|
|
59
|
+
abort "! \e[31mERROR\e[0m: #{msg}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def puts_warning msg
|
|
63
|
+
puts "! \e[33mWARNING:\e[0m: #{msg}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Extend IO to readlines without blocking
|
|
67
|
+
class IO
|
|
68
|
+
def gets_nonblock
|
|
69
|
+
@rlnb_buffer ||= ""
|
|
70
|
+
ch = nil
|
|
71
|
+
while ch = self.read_nonblock(1)
|
|
72
|
+
@rlnb_buffer += ch
|
|
73
|
+
if ch == "\n" then
|
|
74
|
+
res = @rlnb_buffer
|
|
75
|
+
@rlnb_buffer = ""
|
|
76
|
+
return res
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Extend Array to get averages
|
|
83
|
+
class Array
|
|
84
|
+
def average
|
|
85
|
+
inject(:+) / count
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Class to hold XDCC requests
|
|
90
|
+
class XDCC_REQ
|
|
91
|
+
attr_accessor :serv, :chan, :bot, :pack, :info
|
|
92
|
+
|
|
93
|
+
def initialize serv, chan, bot, pack, info = "*"
|
|
94
|
+
@serv = serv
|
|
95
|
+
@chan = chan
|
|
96
|
+
@bot = bot
|
|
97
|
+
@pack = pack
|
|
98
|
+
@info = info
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def eql? other
|
|
102
|
+
self.serv == other.serv and self.chan == other.chan and self.bot == other.bot and self.pack == other.pack
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def to_s
|
|
106
|
+
"[ #{self.serv}, #{self.chan}, #{self.bot}, #{self.pack}, #{self.info} ]"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Class to hold DCC SEND info for when waiting for DCC ACCEPT
|
|
111
|
+
class XDCC_SEND
|
|
112
|
+
attr_accessor :fname, :fsize, :ip, :port
|
|
113
|
+
|
|
114
|
+
def initialize fname, fsize, ip, port
|
|
115
|
+
@fname = fname
|
|
116
|
+
@fsize = fsize
|
|
117
|
+
@ip = ip
|
|
118
|
+
@port = port
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def to_s
|
|
122
|
+
"[ #{self.fname}, #{self.fsize}, #{self.ip}, #{self.port} ]"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Class to emit events
|
|
127
|
+
module Emitter
|
|
128
|
+
def callbacks
|
|
129
|
+
@callbacks ||= Hash.new { |h, k| h[k] = [] }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def on type, &block
|
|
133
|
+
callbacks[type] << block
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def emit type, *args
|
|
138
|
+
callbacks[type].each do |block|
|
|
139
|
+
block.call(*args)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Class to handle IRC stream and emit events
|
|
145
|
+
class Stream
|
|
146
|
+
include Emitter
|
|
147
|
+
attr_accessor :io, :buf
|
|
148
|
+
|
|
149
|
+
def initialize serv
|
|
150
|
+
@buf = []
|
|
151
|
+
Timeout.timeout(5) { @io = TCPSocket.new serv, 6667 }
|
|
152
|
+
rescue SocketError => e
|
|
153
|
+
puts_abort "Failed to connect to #{serv}! #{e.message}"
|
|
154
|
+
rescue Timeout::Error
|
|
155
|
+
puts_abort "Connection to #{serv} timed out!"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def disconnect
|
|
159
|
+
@io.puts 'QUIT'
|
|
160
|
+
rescue Errno::EPIPE
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def << data
|
|
164
|
+
@buf << data
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def write
|
|
168
|
+
@buf.each do |x|
|
|
169
|
+
@io.puts x
|
|
170
|
+
emit :WROTE, x
|
|
171
|
+
end
|
|
172
|
+
@buf = []
|
|
173
|
+
rescue EOFError, Errno::ECONNRESET
|
|
174
|
+
emit :CLOSED
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def read
|
|
178
|
+
read = @io.gets_nonblock
|
|
179
|
+
emit :READ, read
|
|
180
|
+
rescue IO::WaitReadable
|
|
181
|
+
emit :WAITING
|
|
182
|
+
rescue EOFError, Errno::ECONNRESET
|
|
183
|
+
emit :CLOSED
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Class to handle IRC stream
|
|
188
|
+
class Bot
|
|
189
|
+
attr_reader :stream
|
|
190
|
+
|
|
191
|
+
def initialize stream
|
|
192
|
+
@stream = stream
|
|
193
|
+
stream.on :CLOSED do stop; end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def start
|
|
197
|
+
@running = true
|
|
198
|
+
tick while @running
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def stop
|
|
202
|
+
@running = false
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def tick
|
|
206
|
+
stream.read
|
|
207
|
+
stream.write
|
|
208
|
+
sleep 0.001
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Get relative size from bytes
|
|
213
|
+
def bytes_to_closest bytes
|
|
214
|
+
fsize_arr = [ 'B', 'KB', 'MB', 'GB', 'TB' ]
|
|
215
|
+
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
216
|
+
exp = fsize_arr.length if exp > fsize_arr.length
|
|
217
|
+
bytes /= 1024.0 ** exp
|
|
218
|
+
return "#{bytes.round(2)}#{fsize_arr[exp]}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Loop until there is no file with the same name
|
|
222
|
+
def safe_fname fname
|
|
223
|
+
return fname unless File.exists? fname
|
|
224
|
+
|
|
225
|
+
ext = File.extname fname
|
|
226
|
+
base = File.basename fname, ext
|
|
227
|
+
dir = File.dirname fname
|
|
228
|
+
|
|
229
|
+
cur = 2
|
|
230
|
+
while true
|
|
231
|
+
test = "#{dir}/#{base} (#{cur})#{ext}"
|
|
232
|
+
return test unless File.exists? test
|
|
233
|
+
cur += 1
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Get a close relative time remaining, in words
|
|
238
|
+
def time_distance t
|
|
239
|
+
if t < 60
|
|
240
|
+
case t
|
|
241
|
+
when 0 then "- nevermind, done!"
|
|
242
|
+
when 1..4 then "in a moment!"
|
|
243
|
+
when 5..9 then "less than 10 seconds"
|
|
244
|
+
when 10..19 then "less than 20 seconds"
|
|
245
|
+
when 20..39 then "half a minute"
|
|
246
|
+
else "less than a minute"
|
|
247
|
+
end
|
|
248
|
+
else # Use minutes, to aovid big numbers
|
|
249
|
+
t = t / 60.0
|
|
250
|
+
case t.to_i
|
|
251
|
+
when 1 then "about a minute"
|
|
252
|
+
when 2..45 then "#{t.round} minutes"
|
|
253
|
+
when 45..90 then "about an hour"
|
|
254
|
+
when 91..1440 then "about #{(t / 60.0).round} hours"
|
|
255
|
+
when 1441..2520 then "about a day"
|
|
256
|
+
when 2521..86400 then "about #{(t / 1440.0).round} days"
|
|
257
|
+
else "about #{(t/ 43200.0).round} months"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Get elapsed time in words
|
|
263
|
+
def time_elapsed t
|
|
264
|
+
return "instantly!" if t <= 0
|
|
265
|
+
|
|
266
|
+
# Get the GMTime from seconds and split
|
|
267
|
+
ta = Time.at(t).gmtime.strftime('%S|%M|%H|%-d|%-m|%Y').split('|', 6).collect { |i| i.to_i }
|
|
268
|
+
ta[-1] -= 1970 # fuck the police
|
|
269
|
+
ta[-2] -= 1 # fuck, fuck
|
|
270
|
+
ta[-3] -= 1 # fuck the police
|
|
271
|
+
|
|
272
|
+
# Remove the 0 digets
|
|
273
|
+
i = 0
|
|
274
|
+
ta.reverse.each do |x|
|
|
275
|
+
break if x != 0
|
|
276
|
+
i += 1
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Unit suffixes
|
|
280
|
+
suffix = [ "seconds", "minutes", "hours", "days", "months", "years" ]
|
|
281
|
+
# Don't use plural if x is 1
|
|
282
|
+
plural = ->(x, y) { x == 1 ? y[0..-2] : y }
|
|
283
|
+
# Format string to "value unit"
|
|
284
|
+
format_str = ->(x) { "#{ta[x]} #{plural[ta[x], suffix[x]]}, " }
|
|
285
|
+
|
|
286
|
+
# Form the string
|
|
287
|
+
ta = ta.take(ta.length - i)
|
|
288
|
+
str = ""
|
|
289
|
+
(ta.length - 1).downto(0) { |x| str += format_str[x] }
|
|
290
|
+
"in #{str[0..-3]}"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# DCC download handler
|
|
294
|
+
def dcc_download ip, port, fname, fsize, read = 0
|
|
295
|
+
sock = nil
|
|
296
|
+
begin
|
|
297
|
+
Timeout.timeout(5) { sock = TCPSocket.new ip, port }
|
|
298
|
+
rescue Timeout::Error
|
|
299
|
+
puts_abort "Connection to #{ip} timed out!"
|
|
300
|
+
end
|
|
301
|
+
puts_abort "Failed to connect to \"#{ip}:#{port}\": #{e}" if sock.nil?
|
|
302
|
+
|
|
303
|
+
fsize_clean = bytes_to_closest fsize
|
|
304
|
+
avgs, last_check, start_time = [], Time.now - 2, Time.now
|
|
305
|
+
fh = File.open fname, (read == 0 ? "w" : "a") # Write or append
|
|
306
|
+
baca = read
|
|
307
|
+
|
|
308
|
+
# Form the status bar
|
|
309
|
+
print_bar = ->() {
|
|
310
|
+
print "\r\e[0K>> [ \e[1;35m"
|
|
311
|
+
pc = read.to_f / fsize.to_f * 100.0
|
|
312
|
+
bars = (pc / 5).to_i
|
|
313
|
+
bars.times { print "#" }
|
|
314
|
+
(20 - bars).times { print " " }
|
|
315
|
+
avg = avgs.average * 1024.0
|
|
316
|
+
kecepatan = (read - baca)
|
|
317
|
+
time_rem = time_distance ((fsize - read) / kecepatan) * 1.5
|
|
318
|
+
print "\e[0m ] \e[1;35m#{pc.round(2)}%\e[0m - #{bytes_to_closest read}/#{fsize_clean} \e[37m@\e[0m \e[1;33m#{bytes_to_closest kecepatan}/s\e[0m in \e[37m#{time_rem}\e[0m"
|
|
319
|
+
|
|
320
|
+
baca = read
|
|
321
|
+
last_check = Time.now
|
|
322
|
+
avgs.clear
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
while buf = sock.readpartial(8192)
|
|
326
|
+
read += buf.bytesize
|
|
327
|
+
avgs << buf.bytesize
|
|
328
|
+
print_bar[] if (Time.now - last_check) > 1 and not avgs.empty?
|
|
329
|
+
|
|
330
|
+
begin
|
|
331
|
+
sock.write_nonblock [read].pack('N')
|
|
332
|
+
rescue Errno::EWOULDBLOCK
|
|
333
|
+
rescue Errno::EAGAIN => e
|
|
334
|
+
puts_error "#{File.basename fname} timed out! #{e}"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
fh << buf
|
|
338
|
+
break if read >= fsize
|
|
339
|
+
end
|
|
340
|
+
print_bar.call unless avgs.empty?
|
|
341
|
+
elapsed_time = time_elapsed (Time.now - start_time).to_i
|
|
342
|
+
|
|
343
|
+
sock.close
|
|
344
|
+
fh.close
|
|
345
|
+
|
|
346
|
+
puts "\n! \e[1;32mSUCCESS\e[0m downloaded \e[1;36m#{File.basename fname}\e[0m #{elapsed_time}"
|
|
347
|
+
rescue EOFError, SocketError => e
|
|
348
|
+
puts "\n! ERROR: #{File.basename fname} failed to download! #{e}"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
opts = {"out-dir" => "./"}
|
|
352
|
+
OptionParser.new do |o|
|
|
353
|
+
o.banner = " Usage: #{$0} [options] [value] [links] [--files] [file1:file2:file3]\n"
|
|
354
|
+
o.on '-h', '--help', 'Prints help' do
|
|
355
|
+
puts o
|
|
356
|
+
puts "\n Examples"
|
|
357
|
+
puts " \txget.rb --config config.conf --nick test"
|
|
358
|
+
puts " \txget.rb --files test1.txt:test2.txt:test3.txt"
|
|
359
|
+
puts " \txget.rb #news@irc.rizon.net/ginpachi-sensei/1"
|
|
360
|
+
puts " \txget.rb #news@irc.rizon.net/ginpachi-sensei/41..46"
|
|
361
|
+
puts " \txget.rb #news@irc.rizon.net/ginpachi-sensei/41..46-2"
|
|
362
|
+
puts " \txget.rb #news@irc.rizon.net/ginpachi-sensei/41..46&49..52-2&30"
|
|
363
|
+
exit
|
|
364
|
+
end
|
|
365
|
+
o.on '-v', '--version', 'Print version' do
|
|
366
|
+
puts "#{$0}: v#{$ver_str}"
|
|
367
|
+
exit
|
|
368
|
+
end
|
|
369
|
+
o.on '-c', '--config CONFIG', String, 'Path to config file' do |a|
|
|
370
|
+
opts["config"] = a
|
|
371
|
+
end
|
|
372
|
+
o.on '-u', '--user USER', String, "IRC 'USER' for Ident" do |a|
|
|
373
|
+
opts["user"] = a
|
|
374
|
+
end
|
|
375
|
+
o.on '-n', '--nick NICK', String, "IRC nickname" do |a|
|
|
376
|
+
opts["nick"] = a
|
|
377
|
+
end
|
|
378
|
+
o.on '-p', '--pass PASS', String, "IRC 'PASS' for Ident" do |a|
|
|
379
|
+
opts["pass"] = a
|
|
380
|
+
end
|
|
381
|
+
o.on '-r', '--real NAME', String, "IRC 'Realname' for Ident" do |a|
|
|
382
|
+
opts["real"] = a
|
|
383
|
+
end
|
|
384
|
+
o.on '-s', '--nickserv PASS', String, "Password for Nickserv" do |a|
|
|
385
|
+
opts["nserv"] = a
|
|
386
|
+
end
|
|
387
|
+
o.on '-f', '--files A,B,C', Array, "Paths to file(s) that contain xget commands" do |a|
|
|
388
|
+
opts["files"] = a
|
|
389
|
+
end
|
|
390
|
+
o.on '-o', '--out DIR', String, "Path to output directory to save files to" do |a|
|
|
391
|
+
opts["out-dir"] = a
|
|
392
|
+
end
|
|
393
|
+
o.on '-q', '--allow-queueing', "Wait for pack to start downloading rather than fail immediately when queued" do |a|
|
|
394
|
+
opts["allow-queueing"] = true
|
|
395
|
+
end
|
|
396
|
+
o.on '-w', '--skip-existing', "Skip downloads that already exist" do |a|
|
|
397
|
+
opts["skip-existing"] = true
|
|
398
|
+
end
|
|
399
|
+
o.on '-z', '--sleep INTERVAL', Integer, "Time in seconds to sleep before requesting next pack. Zero for no sleep." do |a|
|
|
400
|
+
opts["sleep-interval"] = a
|
|
401
|
+
end
|
|
402
|
+
end.parse!
|
|
403
|
+
|
|
404
|
+
# Get the config location
|
|
405
|
+
config_loc = opts["config"]
|
|
406
|
+
config_loc = File.expand_path config_loc unless config_loc.nil?
|
|
407
|
+
if config_loc.nil? or not File.exists? config_loc
|
|
408
|
+
config_loc = File.expand_path "~/.xget.conf"
|
|
409
|
+
config_loc = ".xget.conf" unless File.exists? config_loc
|
|
410
|
+
|
|
411
|
+
unless File.exists? config_loc
|
|
412
|
+
puts "ERROR! Invalid config path '#{config_loc}''. Exiting!"
|
|
413
|
+
exit
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Insert config settings from arguments into config hash
|
|
418
|
+
cur_block = "*"
|
|
419
|
+
config["servers"][cur_block] = {}
|
|
420
|
+
%w(user nick pass real nserv).each do |x|
|
|
421
|
+
config["servers"][cur_block][x.to_sym] = opts[x] unless opts[x].nil?
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Check if specified output directory actually exists
|
|
425
|
+
puts_abort "Out directory, \"#{opts["out-dir"]}\" doesn't exist!" unless Dir.exists? opts["out-dir"]
|
|
426
|
+
config["out-dir"] = opts["out-dir"].dup
|
|
427
|
+
config["out-dir"] << "/" unless config["out-dir"][-1] == "/"
|
|
428
|
+
|
|
429
|
+
# Parse config
|
|
430
|
+
config_copies = {}
|
|
431
|
+
File.open(config_loc, "r").each_line do |line|
|
|
432
|
+
next if line.length <= 1 or line[0] == '#'
|
|
433
|
+
|
|
434
|
+
if line =~ /^\[(\S+)\]$/ # Check if header
|
|
435
|
+
cur_block = $1
|
|
436
|
+
if cur_block.include? ',' # Check if header contains more than one server
|
|
437
|
+
tmp_split = cur_block.split(",")
|
|
438
|
+
next unless tmp_split[0] =~ /^(\w+?).(\w+?).(\w+?)$/
|
|
439
|
+
config_copies[tmp_split[0]] = []
|
|
440
|
+
tmp_split.each do |x| # Add all copies to copies hash
|
|
441
|
+
next if x == tmp_split[0] or not x =~ /^(\w+?).(\w+?).(\w+?)$/
|
|
442
|
+
config_copies[tmp_split[0]].push x unless config_copies[tmp_split[0]].include? x
|
|
443
|
+
end
|
|
444
|
+
cur_block = tmp_split[0]
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Set current block to the new header
|
|
448
|
+
config["servers"][cur_block] = {} unless config["servers"].has_key? cur_block
|
|
449
|
+
elsif line =~ /^(\S+)=(.*+?)$/
|
|
450
|
+
# Check if current line is specifying out directory
|
|
451
|
+
case $1
|
|
452
|
+
when "out-dir"
|
|
453
|
+
t_out_dir = File.expand_path $2
|
|
454
|
+
puts_abort "Out directory, \"#{t_out_dir}\" doesn't exist!" unless Dir.exists? t_out_dir
|
|
455
|
+
config[$1] = t_out_dir
|
|
456
|
+
config[$1] << "/" unless config[$1][-1] == "/"
|
|
457
|
+
next
|
|
458
|
+
when "sleep-interval" then config[$1] = $2.to_i
|
|
459
|
+
when "skip-existing" then config[$1] = ($2 == "true")
|
|
460
|
+
when "allow-queueing" then config[$1] = ($2 == "true")
|
|
461
|
+
else
|
|
462
|
+
# Add value to current header, default is *
|
|
463
|
+
t_sym = $1.downcase.to_sym
|
|
464
|
+
config["servers"][cur_block][t_sym] = $2 unless config["servers"][cur_block].has_key? t_sym
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Go through each and make copies of the original
|
|
470
|
+
unless config_copies.empty?
|
|
471
|
+
config_copies.each do |k,v|
|
|
472
|
+
v.each { |x| config["servers"][x] = config["servers"][k] }
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Set the set the command line config options if specified
|
|
477
|
+
config["skip-existing"] = opts["skip-existing"] if opts["skip-existing"]
|
|
478
|
+
config["allow-queueing"] = opts["allow-queueing"] if opts["allow-queueing"]
|
|
479
|
+
config["sleep-interval"] = opts["sleep-interval"] unless opts["sleep-interval"].nil?
|
|
480
|
+
|
|
481
|
+
# Take remaining arguments and all lines from --files arg and put into array
|
|
482
|
+
to_check = ARGV
|
|
483
|
+
if opts['files'] != nil and not opts['files'].empty?
|
|
484
|
+
opts['files'].each do |x|
|
|
485
|
+
File.open(x, "r").each_line { |y| to_check << y.chomp } if File.exists? x
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
if to_check.empty?
|
|
490
|
+
puts opts
|
|
491
|
+
abort "\n No jobs, nothing to do!"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Parse to_check array for valid XDCC links, irc.serv.org/#chan/bot/pack
|
|
495
|
+
tmp_requests = []
|
|
496
|
+
to_check.each do |x|
|
|
497
|
+
if x =~ /^(#\S+)@(irc.\S+.\w+{2,3})\/(\S+)\/([\.&\-\d]+)$/
|
|
498
|
+
chan = $1
|
|
499
|
+
serv = $2
|
|
500
|
+
bot = $3
|
|
501
|
+
info = config["servers"].has_key?(serv) ? serv : "*"
|
|
502
|
+
$4.split('&').each do |y|
|
|
503
|
+
if y =~ /^(\d+)(\.\.\d+(\-\d+)?)?$/
|
|
504
|
+
pack = $1.to_i
|
|
505
|
+
if $2.nil?
|
|
506
|
+
tmp_requests.push XDCC_REQ.new serv, chan, bot, pack, info
|
|
507
|
+
else
|
|
508
|
+
step = $3.nil? ? 1 : $3[1..-1].to_i
|
|
509
|
+
range = $2[2..-1].to_i
|
|
510
|
+
|
|
511
|
+
puts_abort "Invalid range #{pack} to #{range} in \"#{x}\"" if pack > range or pack == range
|
|
512
|
+
|
|
513
|
+
(pack..range).step(step).each do |z|
|
|
514
|
+
tmp_requests.push XDCC_REQ.new serv, chan, bot, z, info
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
else
|
|
520
|
+
puts_abort "#{x} is not a valid XDCC address\n XDCC Address format: #chan@irc.serv.com/bot/pack(s) or ^\/msg irc.serv.com bot xdcc send #id$"
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Remove duplicate entries from requests
|
|
525
|
+
i = j = 0
|
|
526
|
+
to_pop = []
|
|
527
|
+
tmp_requests.each do |x|
|
|
528
|
+
tmp_requests.each do |y|
|
|
529
|
+
to_pop << j if x.eql? y if i != j
|
|
530
|
+
j += 1
|
|
531
|
+
end
|
|
532
|
+
i += 1
|
|
533
|
+
end
|
|
534
|
+
to_pop.each { |x| tmp_requests.delete_at(x) }
|
|
535
|
+
|
|
536
|
+
# Sort requests array to hash, serv {} -> chan {} -> requests []
|
|
537
|
+
requests = {}
|
|
538
|
+
tmp_requests.each do |x|
|
|
539
|
+
requests[x.serv] = [] unless requests.has_key? x.serv
|
|
540
|
+
requests[x.serv] << x
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
if requests.empty?
|
|
544
|
+
puts opts
|
|
545
|
+
abort "\n No jobs, nothing to do!"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Sort requests by pack
|
|
549
|
+
requests.each do |k,v|
|
|
550
|
+
puts "\e[1;33m#{k}\e[0m \e[1;37m->\e[0m"
|
|
551
|
+
v.sort_by { |x| [x.chan, x.bot, x.pack] }.each { |x| puts " #{x}" }
|
|
552
|
+
end
|
|
553
|
+
puts
|
|
554
|
+
|
|
555
|
+
exit 0
|
|
556
|
+
|
|
557
|
+
# H-h-here we g-go...
|
|
558
|
+
requests.each do |k, v|
|
|
559
|
+
req, info = v[0], config["servers"][v[0].info]
|
|
560
|
+
last_chan, cur_req, motd = "", -1, false
|
|
561
|
+
nick_sent, nick_check, nick_valid = false, false, false
|
|
562
|
+
xdcc_sent, xdcc_accepted, xdcc_queued = false, false, false
|
|
563
|
+
xdcc_accept_time, xdcc_ret, req_send_time = nil, nil, nil
|
|
564
|
+
|
|
565
|
+
stream = Stream.new req.serv
|
|
566
|
+
bot = Bot.new stream
|
|
567
|
+
stream << "NICK #{info[:nick]}"
|
|
568
|
+
stream << "USER #{info[:user]} 0 * #{info[:real]}"
|
|
569
|
+
stream << "PASS #{info[:pass]}" unless info[:pass].nil?
|
|
570
|
+
|
|
571
|
+
# Handle read data
|
|
572
|
+
stream.on :READ do |data|
|
|
573
|
+
/^(?:[:](?<prefix>\S+) )?(?<type>\S+)(?: (?!:)(?<dest>.+?))?(?: [:](?<msg>.+))?$/ =~ data
|
|
574
|
+
#puts "\e[1;37m>>\e[0m #{prefix} | #{type} | #{dest} | #{msg}"
|
|
575
|
+
|
|
576
|
+
case type
|
|
577
|
+
when 'NOTICE'
|
|
578
|
+
if dest == 'AUTH'
|
|
579
|
+
if msg =~ /erroneous nickname/i
|
|
580
|
+
puts_error 'Login failed'
|
|
581
|
+
stream.disconnect
|
|
582
|
+
end
|
|
583
|
+
#puts "> \e[1;32m#{msg}\e[0m"
|
|
584
|
+
else
|
|
585
|
+
if prefix =~ /^NickServ!/
|
|
586
|
+
if not nick_sent and info[:nserv] != nil
|
|
587
|
+
stream << "PRIVMSG NickServ :IDENTIFY #{info[:nserv]}"
|
|
588
|
+
nick_sent = true
|
|
589
|
+
elsif nick_sent and not nick_check
|
|
590
|
+
case msg
|
|
591
|
+
when /password incorrect/i
|
|
592
|
+
nick_valid = false
|
|
593
|
+
nick_check = true
|
|
594
|
+
when /password accepted/i
|
|
595
|
+
nick_valid = true
|
|
596
|
+
nick_check = true
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
#puts "> \e[1;33m#{msg}\e[0m"
|
|
600
|
+
elsif prefix =~ /^#{Regexp.escape req.bot}!(.*)$/i
|
|
601
|
+
case msg
|
|
602
|
+
when /already requested that pack/i, /closing connection/i, /you have a dcc pending/i
|
|
603
|
+
puts_error msg
|
|
604
|
+
stream << "PRIVMSG #{req.bot} :XDCC CANCEL"
|
|
605
|
+
stream << 'QUIT'
|
|
606
|
+
when /you can only have (\d+?) transfer at a time/i
|
|
607
|
+
if config["allow-queueing"]
|
|
608
|
+
puts "! #{prefix}: #{msg}"
|
|
609
|
+
puts_warning "Pack queued, waiting for transfer to start..."
|
|
610
|
+
xdcc_queued = true
|
|
611
|
+
else
|
|
612
|
+
puts_error msg
|
|
613
|
+
stream << "PRIVMSG #{req.bot} :XDCC CANCEL"
|
|
614
|
+
stream << 'QUIT'
|
|
615
|
+
end
|
|
616
|
+
else
|
|
617
|
+
puts "! #{prefix}: #{msg}"
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
when 'PRIVMSG'
|
|
622
|
+
if xdcc_sent and not xdcc_accepted and prefix =~ /#{Regexp.escape req.bot}!(.*)$/i
|
|
623
|
+
/^\001DCC SEND (?<fname>((".*?").*?|(\S+))) (?<ip>\d+) (?<port>\d+) (?<fsize>\d+)\001\015$/ =~ msg
|
|
624
|
+
unless $~.nil?
|
|
625
|
+
req_send_time = nil
|
|
626
|
+
|
|
627
|
+
tmp_fname = fname
|
|
628
|
+
fname = $1 if tmp_fname =~ /^"(.*)"$/
|
|
629
|
+
puts "Preparing to download: \e[1;36m#{fname}\e[0m"
|
|
630
|
+
fname = (config["out-dir"].dup << fname)
|
|
631
|
+
xdcc_ret = XDCC_SEND.new fname, fsize.to_i, [ip.to_i].pack('N').unpack('C4') * '.', port.to_i
|
|
632
|
+
|
|
633
|
+
# Check if the for unfinished download amd try to resume
|
|
634
|
+
if File.exists? xdcc_ret.fname and File.stat(xdcc_ret.fname).size < xdcc_ret.fsize
|
|
635
|
+
stream << "PRIVMSG #{req.bot} :\001DCC RESUME #{tmp_fname} #{xdcc_ret.port} #{File.stat(xdcc_ret.fname).size}\001"
|
|
636
|
+
xdcc_accepted = true
|
|
637
|
+
print "! Incomplete file detected. Attempting to resume..."
|
|
638
|
+
next # Skip and wait for "DCC ACCEPT"
|
|
639
|
+
elsif File.exists? xdcc_ret.fname
|
|
640
|
+
if config["skip-existing"]
|
|
641
|
+
puts_warning "File already exists, skipping..."
|
|
642
|
+
stream << "PRIVMSG #{req.bot} :XDCC CANCEL"
|
|
643
|
+
|
|
644
|
+
xdcc_sent, xdcc_accepted, xdcc_queued = false, false, false
|
|
645
|
+
xdcc_accept_time, xdcc_ret = nil, nil
|
|
646
|
+
next
|
|
647
|
+
else
|
|
648
|
+
puts_warnings "File already existing, using a safe name..."
|
|
649
|
+
xdcc_ret.fname = safe_fname xdcc_ret.fname
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# It's a new download, start from beginning
|
|
654
|
+
Thread.new do
|
|
655
|
+
pid = fork do
|
|
656
|
+
puts "Connecting to: \e[1;34m#{req.bot}\e[0m @ #{xdcc_ret.ip}:#{xdcc_ret.port}"
|
|
657
|
+
dcc_download xdcc_ret.ip, xdcc_ret.port, xdcc_ret.fname, xdcc_ret.fsize
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
Process.wait pid
|
|
661
|
+
xdcc_sent, xdcc_accepted, xdcc_queued = false, false, false
|
|
662
|
+
xdcc_accept_time, xdcc_ret = nil, nil
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
elsif xdcc_accepted and xdcc_ret != nil and msg =~ /^\001DCC ACCEPT ((".*?").*?|(\S+)) (\d+) (\d+)\001\015$/
|
|
666
|
+
# DCC RESUME request accepted, continue the download!
|
|
667
|
+
xdcc_accept_time = nil
|
|
668
|
+
xdcc_accepted = false
|
|
669
|
+
puts "\e[1;32mSUCCESS\e[0m!"
|
|
670
|
+
|
|
671
|
+
Thread.new do
|
|
672
|
+
pid = fork do
|
|
673
|
+
puts "Connecting to: #{req.bot} @ #{xdcc_ret.ip}:#{xdcc_ret.port}"
|
|
674
|
+
dcc_download xdcc_ret.ip, xdcc_ret.port, xdcc_ret.fname, xdcc_ret.fsize, File.stat(xdcc_ret.fname).size
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
Process.wait pid
|
|
678
|
+
xdcc_sent, xdcc_accepted, xdcc_queued = false, false, false
|
|
679
|
+
xdcc_accept_time, xdcc_ret = nil, nil
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
when /^\d+?$/
|
|
683
|
+
type_i = type.to_i
|
|
684
|
+
case type_i
|
|
685
|
+
# when 1 # Print welcome message, because it's nice
|
|
686
|
+
# msg.sub!(/#{Regexp.escape info[:nick]}/, "\e[34m#{info[:nick]}\e[0m")
|
|
687
|
+
# puts "! #{msg}"
|
|
688
|
+
when 400..533 # Handle errors, except a few
|
|
689
|
+
next if [439, 462, 477].include? type_i
|
|
690
|
+
puts_error "#{msg}"
|
|
691
|
+
stream.disconnect
|
|
692
|
+
when 376 then motd = true # Mark the end of the MOTD
|
|
693
|
+
end
|
|
694
|
+
when 'PING' then stream << "PONG :#{msg}"
|
|
695
|
+
when 'ERROR' then (msg =~ /closing link/i ? puts(msg) : puts_error(msg))
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# Handle things while waiting for data
|
|
700
|
+
stream.on :WAITING do
|
|
701
|
+
unless xdcc_accepted
|
|
702
|
+
if motd and not xdcc_sent
|
|
703
|
+
cur_req += 1
|
|
704
|
+
if cur_req >= v.length
|
|
705
|
+
stream.disconnect
|
|
706
|
+
next
|
|
707
|
+
end
|
|
708
|
+
req = v[cur_req]
|
|
709
|
+
|
|
710
|
+
if req.chan != last_chan
|
|
711
|
+
stream << "PART #{last_chan}" unless last_chan == ""
|
|
712
|
+
last_chan = req.chan
|
|
713
|
+
stream << "JOIN #{req.chan}"
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Cooldown between downloads
|
|
717
|
+
if cur_req > 0
|
|
718
|
+
puts "Sleeping for #{config["sleep-interval"]} seconds before requesting the next pack"
|
|
719
|
+
sleep(config["sleep-interval"])
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
stream << "PRIVMSG #{req.bot} :XDCC SEND #{req.pack}"
|
|
723
|
+
req_send_time = Time.now
|
|
724
|
+
xdcc_sent = true
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Wait 25 seconds for DCC SEND response, if there isn't one, abort
|
|
728
|
+
if xdcc_sent and not req_send_time.nil? and not xdcc_accepted
|
|
729
|
+
if config["allow-queueing"] and xdcc_queued
|
|
730
|
+
next
|
|
731
|
+
end
|
|
732
|
+
if (Time.now - req_send_time).floor > 25
|
|
733
|
+
puts_error "#{req.bot} took too long to respond, are you sure it's a bot?"
|
|
734
|
+
stream.disconnect
|
|
735
|
+
bot.stop
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Wait 25 seconds for a DCC ACCEPT response, if there isn't one, don't resume
|
|
740
|
+
if xdcc_sent and xdcc_accepted and not xdcc_accept_time.nil?
|
|
741
|
+
if (Time.now - xdcc_accept_time).floor > 25
|
|
742
|
+
puts "FAILED! Bot client doesn't support resume!"
|
|
743
|
+
puts "Connecting to: #{req.bot} @ #{xdcc_ret.ip}:#{xdcc_ret.port}"
|
|
744
|
+
dcc_download xdcc_ret.ip, xdcc_ret.port, xdcc_ret.fname, xdcc_ret.fsize
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Print sent data, for debugging only really
|
|
751
|
+
stream.on :WROTE do |data|
|
|
752
|
+
#puts "\e[1;37m<<\e[0m #{data}"
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Start the bot
|
|
756
|
+
bot.start
|
|
757
|
+
end
|