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 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
@@ -0,0 +1,3 @@
1
+ .yardoc
2
+ doc
3
+
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ source 'https://rubygems.org'
data/Gemfile.lock ADDED
@@ -0,0 +1,8 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+
5
+ PLATFORMS
6
+ ruby
7
+
8
+ DEPENDENCIES
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,10 @@
1
+ module Torckapi
2
+ class Error < StandardError; end
3
+ class InvalidInfohashError < Error; end
4
+
5
+ module Tracker
6
+ class Error < Torckapi::Error; end
7
+ class InvalidSchemeError < Error; end
8
+ class CommunicationTimeoutError < Error; end
9
+ end
10
+ end
@@ -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,10 @@
1
+ require_relative 'base'
2
+
3
+ module Torckapi
4
+ module Tracker
5
+
6
+ # @todo implement
7
+ class HTTP < Base
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,3 @@
1
+ module Torckapi
2
+ VERSION = "0.0.1"
3
+ 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: