firecracker 1.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.
- data/.gitignore +17 -0
- data/.rspec +5 -0
- data/Gemfile +4 -0
- data/README.md +123 -0
- data/Rakefile +2 -0
- data/firecracker.gemspec +26 -0
- data/lib/firecracker.rb +108 -0
- data/lib/firecracker/base.rb +56 -0
- data/lib/firecracker/tcp_scraper.rb +95 -0
- data/lib/firecracker/udp_scraper.rb +90 -0
- data/lib/firecracker/version.rb +3 -0
- data/spec/firecracker_spec.rb +18 -0
- data/spec/fixtures/example.torrent +0 -0
- data/spec/fixtures/vcr_cassettes/example-total.yml +0 -0
- data/spec/fixtures/vcr_cassettes/example1.yml +0 -0
- data/spec/fixtures/vcr_cassettes/example2.yml +339 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/tcp_spec.rb +65 -0
- data/spec/udp_spec.rb +31 -0
- metadata +127 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# Firecracker
|
2
|
+
|
3
|
+
An implementation of the [UDP](http://bittorrent.org/beps/bep_0015.html)/[TCP](http://wiki.theory.org/BitTorrentSpecification#Tracker_.27scrape.27_Convention) torrent scrape protocol.
|
4
|
+
|
5
|
+
## Get started
|
6
|
+
|
7
|
+
All methods below returns a hash similar to this one.
|
8
|
+
|
9
|
+
``` ruby
|
10
|
+
{
|
11
|
+
seeders: 123,
|
12
|
+
leechers: 456,
|
13
|
+
downloads: 789
|
14
|
+
}
|
15
|
+
```
|
16
|
+
|
17
|
+
### Specify a protocol
|
18
|
+
|
19
|
+
A second argument may be passed to `load`, `raw` and `calculate` to specify which protocol to use.
|
20
|
+
An example argument would look like this: `[:tcp, :udp]`, both tcp and udp are defaults.
|
21
|
+
|
22
|
+
### A local torrent file
|
23
|
+
|
24
|
+
``` ruby
|
25
|
+
Firecracker.load("path/to/file.torrent")
|
26
|
+
```
|
27
|
+
|
28
|
+
### A raw torrent string
|
29
|
+
|
30
|
+
``` ruby
|
31
|
+
torrent = RestClient.get("http://mysite.com/file.torrent")
|
32
|
+
Firecracker.raw(torrent)
|
33
|
+
```
|
34
|
+
|
35
|
+
### A String#bdecode hash
|
36
|
+
|
37
|
+
``` ruby
|
38
|
+
torrent = RestClient.get("http://mysite.com/file.torrent")
|
39
|
+
Firecracker.calculate(torrent.bdecode)
|
40
|
+
```
|
41
|
+
|
42
|
+
## Helper methods
|
43
|
+
|
44
|
+
Ingoing argument (`torrent`) is from now on a [String#bdecode](https://github.com/naquad/bencode_ext) hash.
|
45
|
+
|
46
|
+
``` ruby
|
47
|
+
require "bencode_ext"
|
48
|
+
torrent = File.read("path/to/file.torrent").bdecode
|
49
|
+
```
|
50
|
+
|
51
|
+
### Generate a info_hash string
|
52
|
+
|
53
|
+
``` ruby
|
54
|
+
Firecracker.hash(torrent)
|
55
|
+
# => "03db8637a8e16f7d5e3e4f7557d5d87b1905dc16"
|
56
|
+
```
|
57
|
+
|
58
|
+
### A list of TCP/UDP trackers
|
59
|
+
|
60
|
+
``` ruby
|
61
|
+
Firecracker.udp_trackers(torrent)
|
62
|
+
# => ["udp://tracker.openbittorrent.com:80", "..."]
|
63
|
+
|
64
|
+
Firecracker.tcp_trackers(torrent)
|
65
|
+
# => ["http://torrent.ubuntu.com:6969/scrape", "..."]
|
66
|
+
```
|
67
|
+
|
68
|
+
## UDP/TCP requests
|
69
|
+
|
70
|
+
If you want to define your own server or/and protocol you can do this using the [TCPScraper](https://github.com/oleander/firecracker/blob/master/lib/firecracker/tcp_scraper.rb) and [UDPScraper](https://github.com/oleander/firecracker/blob/master/lib/firecracker/udp_scraper.rb) classes.
|
71
|
+
|
72
|
+
The hash being passed is a [info_hash](http://wiki.theory.org/BitTorrent_Tracker_Protocol) string.
|
73
|
+
|
74
|
+
You can in theory pass up to 72 hashes in one request.
|
75
|
+
|
76
|
+
Keep in mind that if one of the passed hashes is invalid or doesn't exist, the requested server might return 404 or 400.
|
77
|
+
It's therefore recommended to make one request for each hash.
|
78
|
+
|
79
|
+
### TCP
|
80
|
+
|
81
|
+
``` ruby
|
82
|
+
Firecracker::TCPScraper.new({
|
83
|
+
tracker: "exodus.desync.com:6969/announce",
|
84
|
+
hashes: ["c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0"]
|
85
|
+
}).process!
|
86
|
+
|
87
|
+
# {
|
88
|
+
# c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0: {
|
89
|
+
# seeders: 123,
|
90
|
+
# leechers, 456
|
91
|
+
# downloads: 789
|
92
|
+
# }
|
93
|
+
# }
|
94
|
+
```
|
95
|
+
|
96
|
+
### UDP
|
97
|
+
|
98
|
+
``` ruby
|
99
|
+
Firecracker::UDPScraper.new({
|
100
|
+
tracker: "tracker.openbittorrent.com",
|
101
|
+
hashes: ["523d83e8aee1a979e66584b5304d2e8fdc9a1675"]
|
102
|
+
}).process!
|
103
|
+
|
104
|
+
# {
|
105
|
+
# 523d83e8aee1a979e66584b5304d2e8fdc9a1675: {
|
106
|
+
# seeders: 123,
|
107
|
+
# leechers, 456
|
108
|
+
# downloads: 789
|
109
|
+
# }
|
110
|
+
# }
|
111
|
+
```
|
112
|
+
|
113
|
+
## How to install
|
114
|
+
|
115
|
+
[sudo] gem install firecracker
|
116
|
+
|
117
|
+
## Requirements
|
118
|
+
|
119
|
+
Ruby *1.9.2*.
|
120
|
+
|
121
|
+
## License
|
122
|
+
|
123
|
+
*Firecracker* is released under the *MIT license*.
|
data/Rakefile
ADDED
data/firecracker.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/firecracker/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Linus Oleander"]
|
6
|
+
gem.email = ["linus@oleander.nu"]
|
7
|
+
gem.description = %q{An implementation of the UDP/TCP torrent scrape protocol}
|
8
|
+
gem.summary = %q{An implementation of the UDP/TCP torrent scrape protocol}
|
9
|
+
gem.homepage = "https://github.com/oleander.nu/firecracker-rb"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "firecracker"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Firecracker::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency("rest-client")
|
19
|
+
gem.add_dependency("bencode_ext")
|
20
|
+
|
21
|
+
gem.add_development_dependency("vcr")
|
22
|
+
gem.add_development_dependency("rspec")
|
23
|
+
gem.add_development_dependency("webmock")
|
24
|
+
|
25
|
+
gem.required_ruby_version = "~> 1.9.0"
|
26
|
+
end
|
data/lib/firecracker.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require "firecracker/udp_scraper"
|
2
|
+
require "firecracker/tcp_scraper"
|
3
|
+
require "digest/sha1"
|
4
|
+
require "bencode_ext"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module Firecracker
|
8
|
+
#
|
9
|
+
# @torrent String Path to a torrent file
|
10
|
+
# @protocols Array<Symbol> Protocols that should be used. UDP is the fastest.
|
11
|
+
# @return Hash Seeders, leechers and the amounts of downloads
|
12
|
+
#
|
13
|
+
def self.load(torrent, protocols = [:udp, :tcp])
|
14
|
+
Firecracker.raw(File.read(torrent))
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# @torrent String A raw torrent file.
|
19
|
+
# @protocols Array<Symbol> Protocols that should be used. UDP is the fastest.
|
20
|
+
# @return Hash Seeders, leechers and the amounts of downloads
|
21
|
+
#
|
22
|
+
def self.raw(raw, protocols = [:udp, :tcp])
|
23
|
+
Firecracker.calculate(raw.bdecode)
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# @torrent Hash A Torrent hash generated using String#bdecode
|
28
|
+
# @protocols Array<Symbol> Protocols that should be used. UDP is the fastest.
|
29
|
+
# @return Hash Seeders, leechers and the amounts of downloads
|
30
|
+
#
|
31
|
+
def self.calculate(torrent, protocols = [:udp, :tcp])
|
32
|
+
raise "At least one protocol needs to be passed" if protocols.empty?
|
33
|
+
|
34
|
+
# UDP related trackers
|
35
|
+
if protocols.include?(:udp)
|
36
|
+
trackers = udp_trackers(torrent)
|
37
|
+
udp_results = trackers.map do |tracker|
|
38
|
+
begin
|
39
|
+
Firecracker::UDPScraper.new({
|
40
|
+
tracker: tracker,
|
41
|
+
hashes: [hash(torrent)]
|
42
|
+
}).process!
|
43
|
+
rescue
|
44
|
+
# raise $! unless silent
|
45
|
+
end
|
46
|
+
end.reject(&:nil?).map(&:values).flatten
|
47
|
+
end
|
48
|
+
|
49
|
+
# TCP related trackers
|
50
|
+
if protocols.include?(:tcp)
|
51
|
+
trackers = tcp_trackers(torrent)
|
52
|
+
tcp_results = trackers.map do |tracker|
|
53
|
+
begin
|
54
|
+
Firecracker::TCPScraper.new({
|
55
|
+
tracker: tracker,
|
56
|
+
hashes: [hash(torrent)]
|
57
|
+
}).process!
|
58
|
+
rescue
|
59
|
+
# raise $! unless silent
|
60
|
+
end
|
61
|
+
end.reject(&:nil?).map(&:values).flatten
|
62
|
+
end
|
63
|
+
|
64
|
+
(tcp_results + udp_results).inject{ |memo, el| memo.merge(el){ |k, old_v, new_v| old_v + new_v } }
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# @torrent Hash A Torrent hash generated using String#bdecode
|
69
|
+
# @return Array<String> A list of UDP trackers for the given torrent
|
70
|
+
#
|
71
|
+
def self.udp_trackers(torrent)
|
72
|
+
announce = torrent["announce"]
|
73
|
+
announce_list = torrent["announce-list"]
|
74
|
+
|
75
|
+
trackers = announce_list.flatten.select{|tracker| tracker.match(/^udp:\/\//)}
|
76
|
+
if announce.match(/^udp:\/\//)
|
77
|
+
trackers << announce
|
78
|
+
end
|
79
|
+
|
80
|
+
return trackers
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# @torrent Hash A Torrent hash generated using String#bdecode
|
85
|
+
# @return Array<String> A list of TCP trackers for the given torrent
|
86
|
+
#
|
87
|
+
def self.tcp_trackers(torrent)
|
88
|
+
announce = torrent["announce"]
|
89
|
+
announce_list = torrent["announce-list"]
|
90
|
+
|
91
|
+
trackers = announce_list.flatten.select{|tracker| tracker.match(/^http:\/\//)}
|
92
|
+
if announce.match(/^http:\/\//)
|
93
|
+
trackers << announce
|
94
|
+
end
|
95
|
+
|
96
|
+
# TPBs tracker is no longer active
|
97
|
+
return trackers.map{|t| t.gsub(/announce/, "scrape")}.reject{|t| t.match(%r{thepiratebay})}
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# @torrent Hash A Torrent hash generated using String#bdecode
|
102
|
+
# @return String An info_hash. Read more about it here:
|
103
|
+
# http://wiki.theory.org/BitTorrent_Tracker_Protocol
|
104
|
+
#
|
105
|
+
def self.hash(torrent)
|
106
|
+
Digest::SHA1.hexdigest(torrent["info"].bencode)
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "bencode_ext"
|
2
|
+
require "timeout"
|
3
|
+
|
4
|
+
module Firecracker
|
5
|
+
class Base
|
6
|
+
#
|
7
|
+
# @args Hash A bunch of options
|
8
|
+
# Example:
|
9
|
+
# {
|
10
|
+
# tracker: "tracker.ccc.de",
|
11
|
+
# hashes: ["2bbf3d63e6b313ecf2655067b51e93f17eeeb135"],
|
12
|
+
# debug: false
|
13
|
+
# }
|
14
|
+
#
|
15
|
+
def initialize(args = {})
|
16
|
+
@options = {
|
17
|
+
debug: false,
|
18
|
+
timeout: 2,
|
19
|
+
tracker: nil,
|
20
|
+
hashes: []
|
21
|
+
}.merge(args)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# @return Do we have everyting that's needed
|
26
|
+
# to do the request.
|
27
|
+
#
|
28
|
+
def valid?
|
29
|
+
[
|
30
|
+
@options[:tracker],
|
31
|
+
@options[:hashes].count.between?(1, 72)
|
32
|
+
].all?
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# @return Should we print debug ouput?
|
37
|
+
#
|
38
|
+
def debug?
|
39
|
+
@options[:debug]
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# @return Array<String> A list of hashes
|
44
|
+
#
|
45
|
+
def hashes
|
46
|
+
@options[:hashes]
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# @return Integer Global timeout limit
|
51
|
+
#
|
52
|
+
def timeout
|
53
|
+
@options[:timeout]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "rest-client"
|
2
|
+
require "uri"
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Firecracker
|
6
|
+
class TCPScraper < Firecracker::Base
|
7
|
+
#
|
8
|
+
# @return Hash
|
9
|
+
# Example: {
|
10
|
+
# c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0: {
|
11
|
+
# downloads: 123,
|
12
|
+
# complete: 456,
|
13
|
+
# incomplete: 789
|
14
|
+
# }
|
15
|
+
# }
|
16
|
+
#
|
17
|
+
def process!
|
18
|
+
raise "both #tracker and #hashes/#hash must be set" unless valid?
|
19
|
+
|
20
|
+
keys = ["downloaded", "complete", "incomplete"]
|
21
|
+
map = {:complete => :seeders, :incomplete => :leechers, :downloaded => :downloads}
|
22
|
+
results = Hash.new { |h,k| h[k] = 0 }
|
23
|
+
|
24
|
+
raise %q{
|
25
|
+
Someting went wrong.
|
26
|
+
You've passed multiply hashes, but the
|
27
|
+
tracker only responed with one result.
|
28
|
+
} if files.empty? and not @options[:hashes].one?
|
29
|
+
|
30
|
+
if files.empty?
|
31
|
+
keys.each do |key|
|
32
|
+
replace = map[key.to_sym] ? map[key.to_sym] : key
|
33
|
+
results[replace.to_sym] += raw_hash[key].to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
return {
|
37
|
+
hashes.first => results
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
files.keys.each do |key|
|
42
|
+
file = files[key]
|
43
|
+
results.merge!({
|
44
|
+
key => {
|
45
|
+
downloads: file["downloaded"],
|
46
|
+
seeders: file["complete"],
|
47
|
+
leechers: file["incomplete"]
|
48
|
+
}
|
49
|
+
})
|
50
|
+
end
|
51
|
+
|
52
|
+
return results
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def files
|
57
|
+
@_files ||= raw_hash["files"] || {}
|
58
|
+
end
|
59
|
+
|
60
|
+
def raw_hash
|
61
|
+
@_raw_hash ||= if data.nil? or data.empty?
|
62
|
+
{}
|
63
|
+
else
|
64
|
+
count = 0
|
65
|
+
data.gsub(/20:(.+?)d8/) do |m|
|
66
|
+
hash = hashes[count]
|
67
|
+
res = "#{hash.length}:#{hash}d8"
|
68
|
+
count += 1; res
|
69
|
+
end.bdecode || {}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def random_value(max = 20)
|
74
|
+
(0...max).map{ ("a".."z").to_a[rand(26)] }.join
|
75
|
+
end
|
76
|
+
|
77
|
+
def hash_info
|
78
|
+
@_hash_info ||= @options[:hashes].map do |hash|
|
79
|
+
"info_hash=%s" % URI.encode([hash].pack("H*"))
|
80
|
+
end.join("&")
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Sometime, I'm not sure when, the timeout value
|
85
|
+
# passed to RestClient is ignored. That is why
|
86
|
+
# the hole method is wrapped inside a Timeout block.
|
87
|
+
# Is there a better solution?
|
88
|
+
#
|
89
|
+
def data
|
90
|
+
Timeout::timeout(timeout) {
|
91
|
+
@_data ||= RestClient.get("#{@options[:tracker]}?%s" % hash_info, timeout: timeout)
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "socket"
|
2
|
+
require_relative "base"
|
3
|
+
|
4
|
+
module Firecracker
|
5
|
+
class UDPScraper < Firecracker::Base
|
6
|
+
#
|
7
|
+
# @return Hash
|
8
|
+
# Example: {
|
9
|
+
# c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0: {
|
10
|
+
# downloads: 123,
|
11
|
+
# complete: 456,
|
12
|
+
# incomplete: 789
|
13
|
+
# }
|
14
|
+
# }
|
15
|
+
#
|
16
|
+
def process!
|
17
|
+
raise "both #tracker and #hashes/#hash must be set" unless valid?
|
18
|
+
|
19
|
+
# Handshake
|
20
|
+
data = send(to_hex(4497486125440, 8) + to_hex(0, 4) + transaction_id)
|
21
|
+
request_error! unless data
|
22
|
+
|
23
|
+
# Main request
|
24
|
+
data = send(data[16..31] + to_hex(2, 4) + transaction_id + hashes.join)
|
25
|
+
request_error! unless data
|
26
|
+
|
27
|
+
index = 16
|
28
|
+
results = {}
|
29
|
+
|
30
|
+
loop do
|
31
|
+
break unless data[index + 23]
|
32
|
+
|
33
|
+
completed = data[(index + 8)..index + 15].to_i(16)
|
34
|
+
leechers = data[(index + 16)..index + 23].to_i(16)
|
35
|
+
seeders = data[index..index + 7].to_i(16)
|
36
|
+
hash = hashes[(index - 16)/24]
|
37
|
+
|
38
|
+
results.merge!({
|
39
|
+
hash => {
|
40
|
+
downloads: completed,
|
41
|
+
seeders: seeders,
|
42
|
+
leechers: leechers
|
43
|
+
}
|
44
|
+
})
|
45
|
+
|
46
|
+
index += 24
|
47
|
+
end
|
48
|
+
|
49
|
+
return results
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def socket
|
54
|
+
@_socket ||= UDPSocket.open
|
55
|
+
end
|
56
|
+
|
57
|
+
def request_error!
|
58
|
+
raise "Request error. UDP server did not respond"
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_hex(value, max)
|
62
|
+
value.to_s(16).rjust(max * 2, "0")
|
63
|
+
end
|
64
|
+
|
65
|
+
def tracker
|
66
|
+
if @options[:tracker] =~ /^udp:\/\//
|
67
|
+
@options[:tracker]
|
68
|
+
else
|
69
|
+
"udp://#{@options[:tracker]}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def send(data)
|
74
|
+
Timeout::timeout(timeout) {
|
75
|
+
uri = URI.parse(tracker)
|
76
|
+
|
77
|
+
socket.send([data].pack("H*"), 0, uri.host, uri.port || 80)
|
78
|
+
resp = if select([socket], nil, nil, 3)
|
79
|
+
socket.recvfrom_nonblock(65536)
|
80
|
+
end
|
81
|
+
|
82
|
+
resp ? resp.first.unpack("H*").first : nil
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def transaction_id
|
87
|
+
@_transaction_id ||= to_hex(rand(65535), 4)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
describe Firecracker do
|
2
|
+
# use_vcr_cassette "example-total" Crashes Ruby for some reason
|
3
|
+
before(:all) { @result = Firecracker.load("spec/fixtures/example.torrent") }
|
4
|
+
|
5
|
+
let(:keys){
|
6
|
+
[:seeders, :leechers, :downloads]
|
7
|
+
}
|
8
|
+
|
9
|
+
it "should return seeders, leechers and the amount of downloads" do
|
10
|
+
(@result.keys & keys).count.should eq(3)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should only contain values greater than zero" do
|
14
|
+
@result.keys.each do |key|
|
15
|
+
@result[key].should > 0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
Binary file
|
File without changes
|
File without changes
|
@@ -0,0 +1,339 @@
|
|
1
|
+
---
|
2
|
+
- !ruby/struct:VCR::HTTPInteraction
|
3
|
+
request: !ruby/struct:VCR::Request
|
4
|
+
method: :get
|
5
|
+
uri: http://tracker.blazing.de:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
6
|
+
body: !!null
|
7
|
+
headers:
|
8
|
+
accept:
|
9
|
+
- ! '*/*; q=0.5, application/xml'
|
10
|
+
accept-encoding:
|
11
|
+
- gzip, deflate
|
12
|
+
timeout:
|
13
|
+
- '2'
|
14
|
+
response: !ruby/struct:VCR::Response
|
15
|
+
status: !ruby/struct:VCR::ResponseStatus
|
16
|
+
code: 200
|
17
|
+
message: OK
|
18
|
+
headers:
|
19
|
+
content-type:
|
20
|
+
- text/plain
|
21
|
+
content-length:
|
22
|
+
- '88'
|
23
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7193e10:downloadedi10e10:incompletei1130eeee"
|
24
|
+
http_version: '1.1'
|
25
|
+
- !ruby/struct:VCR::HTTPInteraction
|
26
|
+
request: !ruby/struct:VCR::Request
|
27
|
+
method: :get
|
28
|
+
uri: http://tracker.irc.su:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
29
|
+
body: !!null
|
30
|
+
headers:
|
31
|
+
accept:
|
32
|
+
- ! '*/*; q=0.5, application/xml'
|
33
|
+
accept-encoding:
|
34
|
+
- gzip, deflate
|
35
|
+
timeout:
|
36
|
+
- '2'
|
37
|
+
response: !ruby/struct:VCR::Response
|
38
|
+
status: !ruby/struct:VCR::ResponseStatus
|
39
|
+
code: 404
|
40
|
+
message: Not Found
|
41
|
+
headers:
|
42
|
+
content-type:
|
43
|
+
- text/html
|
44
|
+
server:
|
45
|
+
- Microsoft-IIS/7.5
|
46
|
+
x-powered-by:
|
47
|
+
- ASP.NET
|
48
|
+
date:
|
49
|
+
- Mon, 19 Dec 2011 23:04:33 GMT
|
50
|
+
content-length:
|
51
|
+
- '1245'
|
52
|
+
body: ! "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html
|
53
|
+
xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta http-equiv=\"Content-Type\"
|
54
|
+
content=\"text/html; charset=iso-8859-1\"/>\n<title>404 - File or directory
|
55
|
+
not found.</title>\n<style type=\"text/css\">\n<!--\nbody{margin:0;font-size:.7em;font-family:Verdana,
|
56
|
+
Arial, Helvetica, sans-serif;background:#EEEEEE;}\nfieldset{padding:0 15px 10px
|
57
|
+
15px;} \nh1{font-size:2.4em;margin:0;color:#FFF;}\nh2{font-size:1.7em;margin:0;color:#CC0000;}
|
58
|
+
\nh3{font-size:1.2em;margin:10px 0 0 0;color:#000000;} \n#header{width:96%;margin:0
|
59
|
+
0 0 0;padding:6px 2% 6px 2%;font-family:\"trebuchet MS\", Verdana, sans-serif;color:#FFF;\nbackground-color:#555555;}\n#content{margin:0
|
60
|
+
0 0 2%;position:relative;}\n.content-container{background:#FFF;width:96%;margin-top:8px;padding:10px;position:relative;}\n-->\n</style>\n</head>\n<body>\n<div
|
61
|
+
id=\"header\"><h1>Server Error</h1></div>\n<div id=\"content\">\n <div class=\"content-container\"><fieldset>\n
|
62
|
+
\ <h2>404 - File or directory not found.</h2>\n <h3>The resource you are looking
|
63
|
+
for might have been removed, had its name changed, or is temporarily unavailable.</h3>\n
|
64
|
+
</fieldset></div>\n</div>\n</body>\n</html>\n"
|
65
|
+
http_version: '1.1'
|
66
|
+
- !ruby/struct:VCR::HTTPInteraction
|
67
|
+
request: !ruby/struct:VCR::Request
|
68
|
+
method: :get
|
69
|
+
uri: http://tracker.ilibr.org:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
70
|
+
body: !!null
|
71
|
+
headers:
|
72
|
+
accept:
|
73
|
+
- ! '*/*; q=0.5, application/xml'
|
74
|
+
accept-encoding:
|
75
|
+
- gzip, deflate
|
76
|
+
timeout:
|
77
|
+
- '2'
|
78
|
+
response: !ruby/struct:VCR::Response
|
79
|
+
status: !ruby/struct:VCR::ResponseStatus
|
80
|
+
code: 200
|
81
|
+
message: OK
|
82
|
+
headers:
|
83
|
+
content-type:
|
84
|
+
- text/plain
|
85
|
+
content-length:
|
86
|
+
- '88'
|
87
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7296e10:downloadedi10e10:incompletei1155eeee"
|
88
|
+
http_version: '1.1'
|
89
|
+
- !ruby/struct:VCR::HTTPInteraction
|
90
|
+
request: !ruby/struct:VCR::Request
|
91
|
+
method: :get
|
92
|
+
uri: http://tracker.csze.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
93
|
+
body: !!null
|
94
|
+
headers:
|
95
|
+
accept:
|
96
|
+
- ! '*/*; q=0.5, application/xml'
|
97
|
+
accept-encoding:
|
98
|
+
- gzip, deflate
|
99
|
+
timeout:
|
100
|
+
- '2'
|
101
|
+
response: !ruby/struct:VCR::Response
|
102
|
+
status: !ruby/struct:VCR::ResponseStatus
|
103
|
+
code: 200
|
104
|
+
message: OK
|
105
|
+
headers:
|
106
|
+
content-type:
|
107
|
+
- text/plain
|
108
|
+
content-length:
|
109
|
+
- '88'
|
110
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7087e10:downloadedi10e10:incompletei1129eeee"
|
111
|
+
http_version: '1.1'
|
112
|
+
- !ruby/struct:VCR::HTTPInteraction
|
113
|
+
request: !ruby/struct:VCR::Request
|
114
|
+
method: :get
|
115
|
+
uri: http://10.rarbg.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
116
|
+
body: !!null
|
117
|
+
headers:
|
118
|
+
accept:
|
119
|
+
- ! '*/*; q=0.5, application/xml'
|
120
|
+
accept-encoding:
|
121
|
+
- gzip, deflate
|
122
|
+
timeout:
|
123
|
+
- '2'
|
124
|
+
response: !ruby/struct:VCR::Response
|
125
|
+
status: !ruby/struct:VCR::ResponseStatus
|
126
|
+
code: 200
|
127
|
+
message: OK
|
128
|
+
headers:
|
129
|
+
content-type:
|
130
|
+
- text/plain
|
131
|
+
content-length:
|
132
|
+
- '88'
|
133
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7296e10:downloadedi10e10:incompletei1155eeee"
|
134
|
+
http_version: '1.1'
|
135
|
+
- !ruby/struct:VCR::HTTPInteraction
|
136
|
+
request: !ruby/struct:VCR::Request
|
137
|
+
method: :get
|
138
|
+
uri: http://pow7.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
139
|
+
body: !!null
|
140
|
+
headers:
|
141
|
+
accept:
|
142
|
+
- ! '*/*; q=0.5, application/xml'
|
143
|
+
accept-encoding:
|
144
|
+
- gzip, deflate
|
145
|
+
timeout:
|
146
|
+
- '2'
|
147
|
+
response: !ruby/struct:VCR::Response
|
148
|
+
status: !ruby/struct:VCR::ResponseStatus
|
149
|
+
code: 200
|
150
|
+
message: OK
|
151
|
+
headers:
|
152
|
+
content-type:
|
153
|
+
- text/plain
|
154
|
+
content-length:
|
155
|
+
- '88'
|
156
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7296e10:downloadedi10e10:incompletei1155eeee"
|
157
|
+
http_version: '1.1'
|
158
|
+
- !ruby/struct:VCR::HTTPInteraction
|
159
|
+
request: !ruby/struct:VCR::Request
|
160
|
+
method: :get
|
161
|
+
uri: http://tracker.token.ro:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
162
|
+
body: !!null
|
163
|
+
headers:
|
164
|
+
accept:
|
165
|
+
- ! '*/*; q=0.5, application/xml'
|
166
|
+
accept-encoding:
|
167
|
+
- gzip, deflate
|
168
|
+
timeout:
|
169
|
+
- '2'
|
170
|
+
response: !ruby/struct:VCR::Response
|
171
|
+
status: !ruby/struct:VCR::ResponseStatus
|
172
|
+
code: 200
|
173
|
+
message: OK
|
174
|
+
headers:
|
175
|
+
content-type:
|
176
|
+
- text/plain
|
177
|
+
content-length:
|
178
|
+
- '88'
|
179
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7297e10:downloadedi10e10:incompletei1156eeee"
|
180
|
+
http_version: '1.1'
|
181
|
+
- !ruby/struct:VCR::HTTPInteraction
|
182
|
+
request: !ruby/struct:VCR::Request
|
183
|
+
method: :get
|
184
|
+
uri: http://fr33dom.h33t.com:3310/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
185
|
+
body: !!null
|
186
|
+
headers:
|
187
|
+
accept:
|
188
|
+
- ! '*/*; q=0.5, application/xml'
|
189
|
+
accept-encoding:
|
190
|
+
- gzip, deflate
|
191
|
+
timeout:
|
192
|
+
- '2'
|
193
|
+
response: !ruby/struct:VCR::Response
|
194
|
+
status: !ruby/struct:VCR::ResponseStatus
|
195
|
+
code: 200
|
196
|
+
message: OK
|
197
|
+
headers: !!null
|
198
|
+
body: d5:filesde5:flagsd20:min_request_intervali1800eee
|
199
|
+
http_version: '1.1'
|
200
|
+
- !ruby/struct:VCR::HTTPInteraction
|
201
|
+
request: !ruby/struct:VCR::Request
|
202
|
+
method: :get
|
203
|
+
uri: http://t2.pow7.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
204
|
+
body: !!null
|
205
|
+
headers:
|
206
|
+
accept:
|
207
|
+
- ! '*/*; q=0.5, application/xml'
|
208
|
+
accept-encoding:
|
209
|
+
- gzip, deflate
|
210
|
+
timeout:
|
211
|
+
- '2'
|
212
|
+
response: !ruby/struct:VCR::Response
|
213
|
+
status: !ruby/struct:VCR::ResponseStatus
|
214
|
+
code: 200
|
215
|
+
message: OK
|
216
|
+
headers:
|
217
|
+
content-type:
|
218
|
+
- text/plain
|
219
|
+
content-length:
|
220
|
+
- '88'
|
221
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7297e10:downloadedi10e10:incompletei1156eeee"
|
222
|
+
http_version: '1.1'
|
223
|
+
- !ruby/struct:VCR::HTTPInteraction
|
224
|
+
request: !ruby/struct:VCR::Request
|
225
|
+
method: :get
|
226
|
+
uri: http://tracker.ilibr.org:6969/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
227
|
+
body: !!null
|
228
|
+
headers:
|
229
|
+
accept:
|
230
|
+
- ! '*/*; q=0.5, application/xml'
|
231
|
+
accept-encoding:
|
232
|
+
- gzip, deflate
|
233
|
+
timeout:
|
234
|
+
- '2'
|
235
|
+
response: !ruby/struct:VCR::Response
|
236
|
+
status: !ruby/struct:VCR::ResponseStatus
|
237
|
+
code: 200
|
238
|
+
message: OK
|
239
|
+
headers:
|
240
|
+
content-type:
|
241
|
+
- text/plain
|
242
|
+
content-length:
|
243
|
+
- '88'
|
244
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7297e10:downloadedi10e10:incompletei1156eeee"
|
245
|
+
http_version: '1.1'
|
246
|
+
- !ruby/struct:VCR::HTTPInteraction
|
247
|
+
request: !ruby/struct:VCR::Request
|
248
|
+
method: :get
|
249
|
+
uri: http://t1.pow7.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
250
|
+
body: !!null
|
251
|
+
headers:
|
252
|
+
accept:
|
253
|
+
- ! '*/*; q=0.5, application/xml'
|
254
|
+
accept-encoding:
|
255
|
+
- gzip, deflate
|
256
|
+
timeout:
|
257
|
+
- '2'
|
258
|
+
response: !ruby/struct:VCR::Response
|
259
|
+
status: !ruby/struct:VCR::ResponseStatus
|
260
|
+
code: 200
|
261
|
+
message: OK
|
262
|
+
headers:
|
263
|
+
content-type:
|
264
|
+
- text/plain
|
265
|
+
content-length:
|
266
|
+
- '88'
|
267
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7297e10:downloadedi10e10:incompletei1156eeee"
|
268
|
+
http_version: '1.1'
|
269
|
+
- !ruby/struct:VCR::HTTPInteraction
|
270
|
+
request: !ruby/struct:VCR::Request
|
271
|
+
method: :get
|
272
|
+
uri: http://ipv4.tracker.harry.lu:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
273
|
+
body: !!null
|
274
|
+
headers:
|
275
|
+
accept:
|
276
|
+
- ! '*/*; q=0.5, application/xml'
|
277
|
+
accept-encoding:
|
278
|
+
- gzip, deflate
|
279
|
+
timeout:
|
280
|
+
- '2'
|
281
|
+
response: !ruby/struct:VCR::Response
|
282
|
+
status: !ruby/struct:VCR::ResponseStatus
|
283
|
+
code: 200
|
284
|
+
message: OK
|
285
|
+
headers:
|
286
|
+
content-type:
|
287
|
+
- text/plain
|
288
|
+
content-length:
|
289
|
+
- '88'
|
290
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7298e10:downloadedi10e10:incompletei1156eeee"
|
291
|
+
http_version: '1.1'
|
292
|
+
- !ruby/struct:VCR::HTTPInteraction
|
293
|
+
request: !ruby/struct:VCR::Request
|
294
|
+
method: :get
|
295
|
+
uri: http://tracker.pow7.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
|
296
|
+
body: !!null
|
297
|
+
headers:
|
298
|
+
accept:
|
299
|
+
- ! '*/*; q=0.5, application/xml'
|
300
|
+
accept-encoding:
|
301
|
+
- gzip, deflate
|
302
|
+
timeout:
|
303
|
+
- '2'
|
304
|
+
response: !ruby/struct:VCR::Response
|
305
|
+
status: !ruby/struct:VCR::ResponseStatus
|
306
|
+
code: 200
|
307
|
+
message: OK
|
308
|
+
headers:
|
309
|
+
content-type:
|
310
|
+
- text/plain
|
311
|
+
content-length:
|
312
|
+
- '88'
|
313
|
+
body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7298e10:downloadedi10e10:incompletei1156eeee"
|
314
|
+
http_version: '1.1'
|
315
|
+
- !ruby/struct:VCR::HTTPInteraction
|
316
|
+
request: !ruby/struct:VCR::Request
|
317
|
+
method: :get
|
318
|
+
uri: http://tracker.pow7.com:80/scrape?info_hash=%02%95$%F34%A2%C0%9F3%05w%CA%9A5x%E1Z1%F0%8B
|
319
|
+
body: !!null
|
320
|
+
headers:
|
321
|
+
accept:
|
322
|
+
- ! '*/*; q=0.5, application/xml'
|
323
|
+
accept-encoding:
|
324
|
+
- gzip, deflate
|
325
|
+
timeout:
|
326
|
+
- '2'
|
327
|
+
response: !ruby/struct:VCR::Response
|
328
|
+
status: !ruby/struct:VCR::ResponseStatus
|
329
|
+
code: 400
|
330
|
+
message: Invalid Request
|
331
|
+
headers:
|
332
|
+
content-type:
|
333
|
+
- text/html
|
334
|
+
content-length:
|
335
|
+
- '31'
|
336
|
+
body: ! '<title>Invalid Request</title>
|
337
|
+
|
338
|
+
'
|
339
|
+
http_version: '1.1'
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "rspec"
|
2
|
+
require "webmock/rspec"
|
3
|
+
require "vcr"
|
4
|
+
require "yaml"
|
5
|
+
require "firecracker"
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.mock_with :rspec
|
9
|
+
config.extend VCR::RSpec::Macros
|
10
|
+
end
|
11
|
+
|
12
|
+
VCR.config do |c|
|
13
|
+
c.cassette_library_dir = "spec/fixtures/vcr_cassettes"
|
14
|
+
c.stub_with :webmock
|
15
|
+
c.default_cassette_options = {
|
16
|
+
record: :new_episodes
|
17
|
+
}
|
18
|
+
c.allow_http_connections_when_no_cassette = true
|
19
|
+
end
|
data/spec/tcp_spec.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
describe Firecracker::TCPScraper do
|
2
|
+
describe "single file" do
|
3
|
+
# use_vcr_cassette "example1" Doesn't work for some reason
|
4
|
+
|
5
|
+
let(:hashes) { ["c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0"] }
|
6
|
+
let(:raw){
|
7
|
+
Firecracker::TCPScraper.new({
|
8
|
+
tracker: "exodus.desync.com:6969/announce",
|
9
|
+
hashes: hashes,
|
10
|
+
debug: true
|
11
|
+
}).process!
|
12
|
+
}
|
13
|
+
|
14
|
+
let(:values) { raw[raw.keys.first] }
|
15
|
+
|
16
|
+
it "should match the number of hashes" do
|
17
|
+
raw.should have(1).keys
|
18
|
+
raw.keys.first.should eq(hashes.first)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should have 3 categories" do
|
22
|
+
values.keys.each do |key|
|
23
|
+
[:downloads, :seeders, :leechers].should include(key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should only contain values greater than zero" do
|
28
|
+
values.keys.each do |key|
|
29
|
+
values[key].should > 0
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "multi file" do
|
35
|
+
use_vcr_cassette "example2"
|
36
|
+
|
37
|
+
let(:hashes) { ["03db8637a8e16f7d5e3e4f7557d5d87b1905dc16"] }
|
38
|
+
let(:raw){
|
39
|
+
Firecracker::TCPScraper.new({
|
40
|
+
tracker: "tracker.pow7.com:80/scrape",
|
41
|
+
hashes: hashes,
|
42
|
+
debug: true
|
43
|
+
}).process!
|
44
|
+
}
|
45
|
+
|
46
|
+
let(:values) { raw[raw.keys.first] }
|
47
|
+
|
48
|
+
it "should match the number of hashes" do
|
49
|
+
raw.should have(1).keys
|
50
|
+
raw.keys.first.should eq(hashes.first)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should have 3 categories" do
|
54
|
+
values.keys.each do |key|
|
55
|
+
[:seeders, :leechers, :downloads].should include(key)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should only contain values greater than zero" do
|
60
|
+
values.keys.each do |key|
|
61
|
+
values[key].should > 0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/spec/udp_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Firecracker::UDPScraper do
|
4
|
+
let(:hashes) { ["523d83e8aee1a979e66584b5304d2e8fdc9a1675"] }
|
5
|
+
|
6
|
+
let(:raw){
|
7
|
+
Firecracker::UDPScraper.new({
|
8
|
+
tracker: "tracker.openbittorrent.com",
|
9
|
+
hashes: hashes
|
10
|
+
}).process!
|
11
|
+
}
|
12
|
+
|
13
|
+
let(:values) { raw[raw.keys.first] }
|
14
|
+
|
15
|
+
it "should match the number of hashes" do
|
16
|
+
raw.should have(1).keys
|
17
|
+
raw.keys.first.should eq(hashes.first)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have 3 categories" do
|
21
|
+
values.keys.each do |key|
|
22
|
+
[:downloads, :seeders, :leechers].should include(key)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should only contain values greater than zero" do
|
27
|
+
values.keys.each do |key|
|
28
|
+
values[key].should > 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: firecracker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Linus Oleander
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-12 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rest-client
|
16
|
+
requirement: &70225212402380 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70225212402380
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: bencode_ext
|
27
|
+
requirement: &70225212401960 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70225212401960
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: vcr
|
38
|
+
requirement: &70225212401540 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70225212401540
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: &70225212401120 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70225212401120
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: webmock
|
60
|
+
requirement: &70225212400700 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70225212400700
|
69
|
+
description: An implementation of the UDP/TCP torrent scrape protocol
|
70
|
+
email:
|
71
|
+
- linus@oleander.nu
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .gitignore
|
77
|
+
- .rspec
|
78
|
+
- Gemfile
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- firecracker.gemspec
|
82
|
+
- lib/firecracker.rb
|
83
|
+
- lib/firecracker/base.rb
|
84
|
+
- lib/firecracker/tcp_scraper.rb
|
85
|
+
- lib/firecracker/udp_scraper.rb
|
86
|
+
- lib/firecracker/version.rb
|
87
|
+
- spec/firecracker_spec.rb
|
88
|
+
- spec/fixtures/example.torrent
|
89
|
+
- spec/fixtures/vcr_cassettes/example-total.yml
|
90
|
+
- spec/fixtures/vcr_cassettes/example1.yml
|
91
|
+
- spec/fixtures/vcr_cassettes/example2.yml
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
- spec/tcp_spec.rb
|
94
|
+
- spec/udp_spec.rb
|
95
|
+
homepage: https://github.com/oleander.nu/firecracker-rb
|
96
|
+
licenses: []
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ~>
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 1.9.0
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 1.8.15
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: An implementation of the UDP/TCP torrent scrape protocol
|
119
|
+
test_files:
|
120
|
+
- spec/firecracker_spec.rb
|
121
|
+
- spec/fixtures/example.torrent
|
122
|
+
- spec/fixtures/vcr_cassettes/example-total.yml
|
123
|
+
- spec/fixtures/vcr_cassettes/example1.yml
|
124
|
+
- spec/fixtures/vcr_cassettes/example2.yml
|
125
|
+
- spec/spec_helper.rb
|
126
|
+
- spec/tcp_spec.rb
|
127
|
+
- spec/udp_spec.rb
|