xget 1.0.2

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