torckapi 0.0.1
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 +7 -0
- data/.gitignore +3 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +8 -0
- data/README.md +37 -0
- data/lib/torckapi/errors.rb +10 -0
- data/lib/torckapi/response/announce.rb +37 -0
- data/lib/torckapi/response/scrape.rb +24 -0
- data/lib/torckapi/tracker/base.rb +28 -0
- data/lib/torckapi/tracker/http.rb +10 -0
- data/lib/torckapi/tracker/udp.rb +77 -0
- data/lib/torckapi/version.rb +3 -0
- data/lib/torckapi.rb +36 -0
- data/torckapi.gemspec +20 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 29d24fdd398ac1eb525d2f7e549b28758503aaf8
|
4
|
+
data.tar.gz: 2166897e0f941f25da66e84c1c8a2fc8dd6ac9e0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bc75dd6ed40fe0d9ee30cad1edb8552fb143d089856e86f26a6266ac0d91de5a5b9dd08e81f28121329bd23d8e1362b7079acfad8f2bc91f17d5d38feac46487
|
7
|
+
data.tar.gz: 0fb9ac7f55e5aafb757d566c57b452b74c90c49c645c3ec20904f43c9d7990cef164248a27b8e5f24d91004bade06dd7275c745fbfbfbba3f196ec7f85611fae
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
source 'https://rubygems.org'
|
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Torckapi — torrent tracker querying API
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
Torckapi is a querying interface to torrent trackers.
|
6
|
+
|
7
|
+
## Synopsis
|
8
|
+
|
9
|
+
```
|
10
|
+
tracker = Torckapi.tracker("udp://generic.url:80/announce")
|
11
|
+
# => #<Torckapi::Tracker::UDP:0x007fb5941f6ef8
|
12
|
+
# @url=#<URI::Generic:0x007fb5941f7038 URL:udp://generic.url:80/announce>,
|
13
|
+
# @options={:timeout=>15, :tries=>3}>
|
14
|
+
```
|
15
|
+
|
16
|
+
### Queries
|
17
|
+
```
|
18
|
+
tracker.announce("0123456789ABCDEF0123456789ABCDEF01234567")
|
19
|
+
# => #<Torckapi::Response::Announce:0x007fa1bc0f11c0
|
20
|
+
# @info_hash="0123456789ABCDEF0123456789ABCDEF01234567",
|
21
|
+
# @leechers=1,
|
22
|
+
# @seeders=1,
|
23
|
+
# @peers=[["127.0.0.1", 54078], ["127.0.0.2", 43666]]>
|
24
|
+
|
25
|
+
tracker.scrape(["0123456789ABCDEF0123456789ABCDEF01234567", "123456789ABCDEF0123456789ABCDEF012345678"])
|
26
|
+
# => #<Torckapi::Response::Scrape:0x007fa1bc0fe320
|
27
|
+
# @data={"0123456789ABCDEF0123456789ABCDEF01234567"=>{:seeders=>3, :completed=>0, :leechers=>13},
|
28
|
+
# "123456789ABCDEF0123456789ABCDEF012345678"=>{:seeders=>4, :completed=>10, :leechers=>8}}>
|
29
|
+
```
|
30
|
+
|
31
|
+
## TODO
|
32
|
+
|
33
|
+
Add tests.
|
34
|
+
|
35
|
+
Add HTTP implementation.
|
36
|
+
|
37
|
+
Document everything.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
|
3
|
+
module Torckapi
|
4
|
+
module Response
|
5
|
+
class Announce
|
6
|
+
# @!attribute [r] info_hash
|
7
|
+
# @return [String] 40-char hexadecimal string
|
8
|
+
# @!attribute [r] leechers
|
9
|
+
# @return [Fixnum] number of leechers
|
10
|
+
# @!attribute [r] peers
|
11
|
+
# @return [Array<IPAddr, Fixnum>] list of peers
|
12
|
+
# @!attribute [r] seeders
|
13
|
+
# @return [Fixnum] number of seeders
|
14
|
+
attr_reader :info_hash, :leechers, :peers, :seeders
|
15
|
+
|
16
|
+
# Construct response object from udp response data
|
17
|
+
# @param info_hash [String] 40-char hexadecimal string
|
18
|
+
# @param data [String] UDP response data (omit action and transaction_id)
|
19
|
+
# @return [Torckapi::Response::Announce] response
|
20
|
+
def self.from_udp info_hash, data
|
21
|
+
leechers, seeders = data[0..7].unpack('L>2')
|
22
|
+
peers = data[8..-1].unpack('a6' * (leechers + seeders)).map { |i| [IPAddr.ntop(i[0..3]), i[4..5].unpack('S>')[0]] }
|
23
|
+
|
24
|
+
new info_hash, leechers, seeders, peers
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def initialize info_hash, leechers, seeders, peers
|
30
|
+
@info_hash = info_hash
|
31
|
+
@leechers = leechers
|
32
|
+
@seeders = seeders
|
33
|
+
@peers = peers
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Torckapi
|
2
|
+
module Response
|
3
|
+
|
4
|
+
# Scrape response
|
5
|
+
class Scrape
|
6
|
+
# @return [Hash<String, Hash>] scrape data
|
7
|
+
attr_reader :data
|
8
|
+
|
9
|
+
# Construct response object from udp response data
|
10
|
+
# @param info_hashes [Array<String>] list of 40-char hexadecimal strings
|
11
|
+
# @param data [String] UDP response data (omit action and transaction_id)
|
12
|
+
# @return [Torckapi::Response::Scrape] response
|
13
|
+
def self.from_udp info_hashes, data
|
14
|
+
new Hash[info_hashes.zip(data.unpack('a12' * (info_hashes.count)).map { |i| Hash[[:seeders, :completed, :leechers].zip i.unpack('L>3').map(&:to_i)] })]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def initialize data
|
20
|
+
@data = data
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Torckapi
|
2
|
+
module Tracker
|
3
|
+
|
4
|
+
# Public interface for torrent trackers
|
5
|
+
class Base
|
6
|
+
# Announce Request
|
7
|
+
# @param info_hash [String] 40-char hexadecimal string
|
8
|
+
# @return [Torckapi::AnnounceResponse] a response object
|
9
|
+
def announce info_hash
|
10
|
+
raise Torckapi::InvalidInfohashError if info_hash !~ /\A[0-9a-f]{40}\z/i
|
11
|
+
end
|
12
|
+
|
13
|
+
# Scrape Request
|
14
|
+
# @param info_hashes [String, Array<String>] A single 40-char hexadecimal string or an array of those
|
15
|
+
# @return [Torckapi::ScrapeResponse] a response object
|
16
|
+
def scrape info_hashes=[]
|
17
|
+
raise Torckapi::InvalidInfohashError if [*info_hashes].any? { |i| i !~ /\A[0-9a-f]{40}\z/i }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def initialize url, options={}
|
23
|
+
@url = url
|
24
|
+
@options = {timeout: 15, tries: 3}.merge(options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'timeout'
|
3
|
+
require 'securerandom'
|
4
|
+
require_relative 'base'
|
5
|
+
|
6
|
+
module Torckapi
|
7
|
+
module Tracker
|
8
|
+
|
9
|
+
# Implementation of http://www.bittorrent.org/beps/bep_0015.html
|
10
|
+
class UDP < Base
|
11
|
+
# connection_id is valid for 1 minute as per protocol
|
12
|
+
CONNECTION_TIMEOUT = 60
|
13
|
+
|
14
|
+
# @see Base#announce
|
15
|
+
def announce info_hash
|
16
|
+
super info_hash
|
17
|
+
|
18
|
+
data = [[info_hash].pack('H*'), SecureRandom.random_bytes(20), [0, 0, 0, 0, 0, 0, -1, 0].pack('Q>3L>4s>')].join
|
19
|
+
|
20
|
+
connect
|
21
|
+
response_body = communicate(1, data)[0] # announce
|
22
|
+
|
23
|
+
Torckapi::Response::Announce.from_udp info_hash, response_body[12..-1]
|
24
|
+
end
|
25
|
+
|
26
|
+
def scrape info_hashes=[]
|
27
|
+
super info_hashes
|
28
|
+
|
29
|
+
data = [*info_hashes].map { |i| [i].pack('H*') }.join
|
30
|
+
|
31
|
+
connect
|
32
|
+
response_body = communicate(2, data)[0] # scrape
|
33
|
+
|
34
|
+
Torckapi::Response::Scrape.from_udp [*info_hashes], response_body[8..-1]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def initialize *args
|
40
|
+
super *args
|
41
|
+
end
|
42
|
+
|
43
|
+
def connect
|
44
|
+
return if @connection_id && @communicated_at.to_i >= Time.now.to_i - CONNECTION_TIMEOUT
|
45
|
+
|
46
|
+
@connection_id = [0x41727101980].pack('Q>')
|
47
|
+
response_body = communicate(0)[0] # connect
|
48
|
+
@connection_id = response_body[8..15]
|
49
|
+
puts "connection_id = #{@connection_id.inspect}" if Torckapi::debug
|
50
|
+
end
|
51
|
+
|
52
|
+
def communicate action, data=nil
|
53
|
+
@socket ||= UDPSocket.new
|
54
|
+
|
55
|
+
transaction_id = SecureRandom.random_bytes(4)
|
56
|
+
packet = [@connection_id, [action].pack('L>'), transaction_id, data].join
|
57
|
+
|
58
|
+
tries = 0
|
59
|
+
response = nil
|
60
|
+
begin
|
61
|
+
Timeout::timeout(@options[:timeout], CommunicationTimeoutError) do
|
62
|
+
puts "--> #{packet.inspect}" if Torckapi::debug
|
63
|
+
@socket.send(packet, 0, @url.host, @url.port)
|
64
|
+
response = @socket.recvfrom(65536)
|
65
|
+
puts "<-- #{response.inspect}" if Torckapi::debug
|
66
|
+
raise TransactionIdMismatchError if transaction_id != response[0][4..7]
|
67
|
+
@communicated_at = Time.now
|
68
|
+
end
|
69
|
+
rescue CommunicationTimeoutError
|
70
|
+
retry if (tries += 1) <= @options[:tries]
|
71
|
+
end
|
72
|
+
|
73
|
+
response
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/torckapi.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'torckapi/version'
|
3
|
+
require 'torckapi/errors'
|
4
|
+
require 'torckapi/tracker/http'
|
5
|
+
require 'torckapi/tracker/udp'
|
6
|
+
require 'torckapi/response/announce'
|
7
|
+
require 'torckapi/response/scrape'
|
8
|
+
|
9
|
+
module Torckapi
|
10
|
+
@@debug = false
|
11
|
+
|
12
|
+
def self.debug
|
13
|
+
@@debug
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.debug= debug
|
17
|
+
@@debug = !!debug
|
18
|
+
end
|
19
|
+
|
20
|
+
# Creates a tracker interface instance
|
21
|
+
# @param tracker_url [String] tracker announce url
|
22
|
+
# @param options [Hash] defaults to \\{timeout: 15, tries: 3}
|
23
|
+
# @return [Torckapi::Tracker::Base] tracker interface instance
|
24
|
+
def self.tracker tracker_url, options={}
|
25
|
+
url = URI.parse tracker_url
|
26
|
+
|
27
|
+
case url.scheme
|
28
|
+
when "http"
|
29
|
+
Torckapi::Tracker::HTTP.new url, options
|
30
|
+
when "udp"
|
31
|
+
Torckapi::Tracker::UDP.new url, options
|
32
|
+
else
|
33
|
+
raise InvalidSchemeError, "'#{tracker_url}' cannot be recognized as valid tracker url"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/torckapi.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$:.unshift "lib"
|
2
|
+
require 'torckapi/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Dennis Krupenik"]
|
6
|
+
gem.email = ["dennis@krupenik.com"]
|
7
|
+
gem.description = %q{torrent tracker api}
|
8
|
+
gem.summary = %q{torckapi is a tool for quering torrent trackers}
|
9
|
+
gem.homepage = "https://github.com/krupenik/torckapi"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "torckapi"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Torckapi::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency 'rake'
|
19
|
+
gem.add_development_dependency 'minitest'
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: torckapi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dennis Krupenik
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-04-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: torrent tracker api
|
42
|
+
email:
|
43
|
+
- dennis@krupenik.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- Gemfile
|
50
|
+
- Gemfile.lock
|
51
|
+
- README.md
|
52
|
+
- lib/torckapi.rb
|
53
|
+
- lib/torckapi/errors.rb
|
54
|
+
- lib/torckapi/response/announce.rb
|
55
|
+
- lib/torckapi/response/scrape.rb
|
56
|
+
- lib/torckapi/tracker/base.rb
|
57
|
+
- lib/torckapi/tracker/http.rb
|
58
|
+
- lib/torckapi/tracker/udp.rb
|
59
|
+
- lib/torckapi/version.rb
|
60
|
+
- torckapi.gemspec
|
61
|
+
homepage: https://github.com/krupenik/torckapi
|
62
|
+
licenses: []
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 2.0.0
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: torckapi is a tool for quering torrent trackers
|
84
|
+
test_files: []
|
85
|
+
has_rdoc:
|