ttwatcher 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +20 -0
- data/LICENSE +9 -0
- data/sources/ttwatcher.rb +70 -0
- data/sources/ttwatcher/helpers.rb +24 -0
- data/sources/ttwatcher/logger.rb +40 -0
- data/sources/ttwatcher/project_structure.rb +50 -0
- data/sources/ttwatcher/sites.rb +68 -0
- data/sources/ttwatcher/sites/config.rb +20 -0
- data/sources/ttwatcher/sites/config.yml +29 -0
- data/sources/ttwatcher/sites/connection.rb +82 -0
- data/sources/ttwatcher/sites/connection/scheme.rb +79 -0
- data/sources/ttwatcher/sites/connection/url.rb +66 -0
- data/sources/ttwatcher/sites/megashara.rb +18 -0
- data/sources/ttwatcher/sites/parsers/abstract_parser.rb +39 -0
- data/sources/ttwatcher/sites/parsers/megashara_parser.rb +64 -0
- data/sources/ttwatcher/sites/parsers/rutor_parser.rb +67 -0
- data/sources/ttwatcher/sites/parsers/simple_parser.rb +134 -0
- data/sources/ttwatcher/sites/parsers/unionpeer_parser.rb +62 -0
- data/sources/ttwatcher/sites/parsers/zooqle_parser.rb +80 -0
- data/sources/ttwatcher/sites/rutor.rb +13 -0
- data/sources/ttwatcher/sites/site.rb +92 -0
- data/sources/ttwatcher/sites/torrent_site.rb +70 -0
- data/sources/ttwatcher/sites/unionpeer.rb +18 -0
- data/sources/ttwatcher/sites/zooqle.rb +18 -0
- data/sources/ttwatcher/torrent.rb +87 -0
- data/sources/ttwatcher/torrent_agent.rb +34 -0
- data/sources/ttwatcher/torrent_list.rb +51 -0
- data/spec/sources/ttwatcher/sites_spec.rb +8 -0
- data/spec/sources/ttwatcher/torrent_list_spec.rb +46 -0
- data/spec/spec_helper.rb +20 -0
- metadata +156 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
module Parsers
|
5
|
+
class Zooqle < SimpleParser
|
6
|
+
private
|
7
|
+
|
8
|
+
def new_pages_list # no-doc
|
9
|
+
return @links if @links.is_a? Array
|
10
|
+
|
11
|
+
tmp = structure.css('ul[@class="pagination smaller pull-right margin-top-20"]')
|
12
|
+
.css('li')
|
13
|
+
return @links = [] if tmp.nil? || tmp.empty?
|
14
|
+
|
15
|
+
url_part = tmp.css('a').attr('href').to_s.gsub(/pg=\d*/,'pg=+NUMBER+')
|
16
|
+
mm = tmp.slice(0..-2).map { |node| node.css('a').text.to_i}.minmax
|
17
|
+
|
18
|
+
@links = (mm.first..mm.last).map do |page_number|
|
19
|
+
'search' + url_part.gsub(/\+NUMBER\+/, page_number.to_s)
|
20
|
+
end.slice!(2..-1) || []
|
21
|
+
end
|
22
|
+
|
23
|
+
def torrents_unparsed # no-doc
|
24
|
+
structure.css('table[@class="table table-condensed table-torrents vmiddle"]')
|
25
|
+
.css('tr').drop(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param [Nokogiri::Node] unparsed_data
|
29
|
+
#
|
30
|
+
# Surface scan for +zooqle+ gives next information about single torrent
|
31
|
+
#
|
32
|
+
# ++ hsh[:name] ==> ex. "Cats swimming in pool 2016 BDRIP"
|
33
|
+
# -- hsh[:description] ==> ex. "Hot CATS. Summer 2016"
|
34
|
+
# ++ hsh[:url] ==> ex. "example.torrent.side/12345"
|
35
|
+
# ++ hsh[:tracker] ==> ex. :super_cool_tracker
|
36
|
+
# -- hsh[:author] ==> ex. 'Bit kitty fun'
|
37
|
+
# -- hsh[:added_date] ==> ex. '2016-06-15'
|
38
|
+
# ++ hsh[:seeders] ==> ex. 50042
|
39
|
+
# ++ hsh[:leeches] ==> ex. 1
|
40
|
+
# ++ hsh[:size] ==> ex. "20000 mb"
|
41
|
+
# ++ hsh[:magnet_url] ==> ex. "magnet:?xt=urn....................."
|
42
|
+
# ++ hsh[:download_url] ==> ex. "example.torrent.side/12345/download"
|
43
|
+
#
|
44
|
+
# Where '++' means that field is present.
|
45
|
+
#
|
46
|
+
# @return [Torrent]
|
47
|
+
|
48
|
+
def extract_torrent(unparsed_data)
|
49
|
+
hsh = Hash.new
|
50
|
+
|
51
|
+
hsh[:name] = unparsed_data.css('td')[1].text
|
52
|
+
|
53
|
+
tmp_td1 = unparsed_data.css('td')[1].css('a').attr('href').to_s
|
54
|
+
hsh[:url] = assigned_site.address tmp_td1
|
55
|
+
|
56
|
+
tmp_td2 = unparsed_data.css('td')[2].css('li')
|
57
|
+
hsh[:magnet_url] = tmp_td2[0].css('a').attr('href')
|
58
|
+
|
59
|
+
if tmp_td2[1]
|
60
|
+
tmp = tmp_td2[1].css('a').attr('href').to_s
|
61
|
+
hsh[:download_url] = assigned_site.address tmp
|
62
|
+
end
|
63
|
+
|
64
|
+
hsh[:size] = unparsed_data.css('td')[3].text
|
65
|
+
|
66
|
+
tmp_td5 = unparsed_data.css('td')[5]
|
67
|
+
.css('div[@class="progress prog trans90"]')
|
68
|
+
.css('div')
|
69
|
+
if tmp_td5[1]
|
70
|
+
hsh[:seeders] = tmp_td5[1].text.to_i
|
71
|
+
hsh[:leeches] = tmp_td5[2].text.to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
hsh[:tracker] = assigned_site.name
|
75
|
+
|
76
|
+
return Torrent.new(hsh)
|
77
|
+
end
|
78
|
+
end # class Zooqle
|
79
|
+
end # module Parsers
|
80
|
+
end # module TTWatcher
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
module Sites
|
5
|
+
class Rutor < TorrentSite # > ex Rutor.org
|
6
|
+
private
|
7
|
+
|
8
|
+
def search_url(name) # no-doc
|
9
|
+
domain_name + '/search/0/0/000/0/%s' % name
|
10
|
+
end
|
11
|
+
end # class Rutor
|
12
|
+
end # module Sites
|
13
|
+
end # module TTWatcher
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
module Sites
|
5
|
+
class Site
|
6
|
+
|
7
|
+
# @return [Symbol]
|
8
|
+
# Site name (autogenerated, depends from class name).
|
9
|
+
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# @return [TTWatcher::Connection]
|
13
|
+
|
14
|
+
attr_reader :connection
|
15
|
+
|
16
|
+
# @return [String]
|
17
|
+
# Returns full domain name.
|
18
|
+
|
19
|
+
def domain_name
|
20
|
+
@domain_name ||=
|
21
|
+
if S[name][:domain_name]
|
22
|
+
S[name][:domain_name]
|
23
|
+
else
|
24
|
+
raise DomainNameNotFound, self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Creates new Site object.
|
30
|
+
#
|
31
|
+
# @return [Site]
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@name = H::class_name self
|
35
|
+
@connection = Connection.new self
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Generates an url that includes scheme + domain name + +path+ (optional)
|
40
|
+
#
|
41
|
+
# example: input ==> @domain_name = "some.site.com", path = 'hello/world'
|
42
|
+
# output ==> "http://some.site.com/hello/world"
|
43
|
+
#
|
44
|
+
# if path not selected it returns scheme + +@domain_name+
|
45
|
+
|
46
|
+
def address(path='')
|
47
|
+
scheme = connection.url.scheme
|
48
|
+
if !path.empty? && domain_name_included?(path)
|
49
|
+
InternetConnection::Scheme.add_scheme!(path, scheme)
|
50
|
+
else
|
51
|
+
hn = domain_name.dup # prevent unnecessary +domain_name+ mutation
|
52
|
+
InternetConnection::Scheme.add_scheme!(hn, scheme)
|
53
|
+
path = '/' + path unless path[0] == '/'
|
54
|
+
hn + path
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [String] url
|
59
|
+
#
|
60
|
+
# @param [Hash] request_params
|
61
|
+
#
|
62
|
+
# @return [String, NilClass]
|
63
|
+
|
64
|
+
def download_page(url, request_params = {})
|
65
|
+
url = address(url) unless domain_name_included? url
|
66
|
+
page = connection.download url, request_params
|
67
|
+
|
68
|
+
return page
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# @param [String] url
|
74
|
+
# Url or an url part.
|
75
|
+
#
|
76
|
+
# @return [TrueClass, FalseClass]
|
77
|
+
# +true+ when url includes domain name
|
78
|
+
# +false+ otherwise.
|
79
|
+
|
80
|
+
def domain_name_included?(url)
|
81
|
+
url[domain_name.to_s]
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Raised when param +domain name+ not found.
|
86
|
+
|
87
|
+
class DomainNameNotFound < TTWError
|
88
|
+
def initialize(klass) "Param ('domain_name') not found for #{klass.class} in config.yml." end
|
89
|
+
end
|
90
|
+
end # class Site
|
91
|
+
end # module Sites
|
92
|
+
end # module TTWatcher
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
module Sites
|
5
|
+
class TorrentSite < Site # no-doc
|
6
|
+
|
7
|
+
# @param [String] name
|
8
|
+
#
|
9
|
+
# @return [TrueClass, FalseClass]
|
10
|
+
# Returns +false+ when +name+ too short or not exists.
|
11
|
+
|
12
|
+
def self.search_request_valid?(name)
|
13
|
+
name && name.length >= 3
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Searches torrents.
|
18
|
+
#
|
19
|
+
# @param [String] name
|
20
|
+
# Torrent name.
|
21
|
+
#
|
22
|
+
# @return [TorrentList, NilClass]
|
23
|
+
|
24
|
+
def find_torrent(name, request_params = {})
|
25
|
+
return nil unless self.class.search_request_valid? name
|
26
|
+
|
27
|
+
html_body = download_page search_url(name), request_params
|
28
|
+
torrents = parser.parse html_body
|
29
|
+
|
30
|
+
return torrents
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
##
|
36
|
+
# Url for torrent searching.
|
37
|
+
# see Sites#search_url for an example.
|
38
|
+
#
|
39
|
+
# @param [String] name
|
40
|
+
# Used to extend url with +name+ when it need.
|
41
|
+
# For an example check Rutor#search_url
|
42
|
+
|
43
|
+
def search_url(name=nil)
|
44
|
+
raise NotImplementedError, "Abstract method called!"
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Returns a parser associated with Site instance throw class names.
|
49
|
+
#
|
50
|
+
# @return [Parsers::Base]
|
51
|
+
|
52
|
+
def parser
|
53
|
+
@parser ||=
|
54
|
+
begin
|
55
|
+
class_name = self.class.name.split('::').last
|
56
|
+
parser = TTWatcher::Parsers.const_get class_name
|
57
|
+
parser.new self
|
58
|
+
end
|
59
|
+
rescue NameError => exception
|
60
|
+
notificate_about_parser_crash! exception
|
61
|
+
|
62
|
+
return nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def notificate_about_parser_crash!(exception)
|
66
|
+
Logger.with_backtrace "Parser #{self} crashed with message: *** #{exception.message} ***"
|
67
|
+
end
|
68
|
+
end # class TorrentSite
|
69
|
+
end # module Sites
|
70
|
+
end # module TTWatcher
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
module Sites
|
5
|
+
class Unionpeer < TorrentSite
|
6
|
+
|
7
|
+
def find_torrent(name) # no-doc
|
8
|
+
super name, { url: { query_params: { 'nm' => name } } }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def search_url(name = nil) # no-doc
|
14
|
+
domain_name + '/tracker.php'
|
15
|
+
end
|
16
|
+
end # class Unionpeer
|
17
|
+
end # module Sites
|
18
|
+
end # module TTWatcher
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
module Sites
|
5
|
+
class Zooqle < TorrentSite
|
6
|
+
|
7
|
+
def find_torrent(name) # no-doc
|
8
|
+
super name, { url: { query_params: { 'q' => name } } }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def search_url(name = nil) # no-doc
|
14
|
+
domain_name + '/search'
|
15
|
+
end
|
16
|
+
end # class Zooqle
|
17
|
+
end # module Sites
|
18
|
+
end # module TTWatcher
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
class Torrent
|
5
|
+
|
6
|
+
# @return [String]
|
7
|
+
# Returns torrent name.
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
# Returns torrent description.
|
13
|
+
|
14
|
+
attr_reader :description
|
15
|
+
|
16
|
+
# @return [String]
|
17
|
+
# Returns url to an page with additional info about torrent.
|
18
|
+
|
19
|
+
attr_reader :url
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
# Returns torrent size.
|
23
|
+
|
24
|
+
attr_reader :size
|
25
|
+
|
26
|
+
# @return [String]
|
27
|
+
# Returns torrent creator.
|
28
|
+
|
29
|
+
attr_reader :author
|
30
|
+
|
31
|
+
# @return [String]
|
32
|
+
# Returns leeches count.
|
33
|
+
|
34
|
+
attr_reader :leeches
|
35
|
+
|
36
|
+
# @return [String]
|
37
|
+
# Returns seeders count.
|
38
|
+
|
39
|
+
attr_reader :seeders
|
40
|
+
|
41
|
+
# @return [String]
|
42
|
+
# Returns date (as string) when torrent was added.
|
43
|
+
|
44
|
+
attr_reader :added_date
|
45
|
+
|
46
|
+
# @return [String]
|
47
|
+
# Returns magnet url to the torrent.
|
48
|
+
|
49
|
+
attr_reader :magnet_url
|
50
|
+
|
51
|
+
# @return [String]
|
52
|
+
# Returns direct url for torrent download.
|
53
|
+
|
54
|
+
attr_reader :download_url
|
55
|
+
|
56
|
+
# @return [Symbol]
|
57
|
+
# Returns tracker for the torrent.
|
58
|
+
|
59
|
+
attr_reader :tracker
|
60
|
+
|
61
|
+
##
|
62
|
+
# Creates new Torrent instance.
|
63
|
+
#
|
64
|
+
# @param [Hash] params
|
65
|
+
# @option params [String] :name
|
66
|
+
# @option params [String] :description
|
67
|
+
# @option params [String] :url.
|
68
|
+
# @option params [Symbol] :tracker
|
69
|
+
# @option params [String] :author
|
70
|
+
# @option params [String] :added_date
|
71
|
+
# @option params [Integer] :seeders
|
72
|
+
# @option params [Integer] :leeches
|
73
|
+
# @option params [String] :size
|
74
|
+
# @option params [String] :magnet_url
|
75
|
+
# @option params [String] :download_url
|
76
|
+
#
|
77
|
+
# @return [Torrent]
|
78
|
+
|
79
|
+
def initialize(params = {})
|
80
|
+
%i(name description url tracker author added_date seeders
|
81
|
+
leeches size magnet_url download_url).each do |word|
|
82
|
+
|
83
|
+
instance_variable_set "@#{word}", params[word]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end # class Torrent
|
87
|
+
end # module TTWatcher
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
module TorrentAgent
|
5
|
+
|
6
|
+
##
|
7
|
+
# TorrentAgent#search
|
8
|
+
# Resolves where exactly to search torrents.
|
9
|
+
#
|
10
|
+
# @param [String] torrent_name
|
11
|
+
# Torrent name (minimal length: 3 chars).
|
12
|
+
#
|
13
|
+
# @param [Hash] params
|
14
|
+
# @option params [Array<Symbol, String>, Symbol, String] :sites
|
15
|
+
# List of sites where we looking for torrent. By default
|
16
|
+
# it search everywhere ("rutor", "unionpeer", "zooqle", "megashara").
|
17
|
+
#
|
18
|
+
# @return [Array<Torrent>]
|
19
|
+
# Array (homogeneous) with torrents. Can be empty if nothing was found.
|
20
|
+
|
21
|
+
def self.search(torrent_name, params = {})
|
22
|
+
list = TorrentList.new
|
23
|
+
|
24
|
+
site_names = params[:from].nil? ? Sites.list : Array(params[:from])
|
25
|
+
site_names.each do |name|
|
26
|
+
site = Sites.fetch_torrent_site name
|
27
|
+
new_torrents = site.new.find_torrent torrent_name
|
28
|
+
list += new_torrents if new_torrents
|
29
|
+
end
|
30
|
+
|
31
|
+
return list.to_a
|
32
|
+
end
|
33
|
+
end # module TorrentsAgent
|
34
|
+
end # module TTWatcher
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
module TTWatcher
|
4
|
+
class TorrentList
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
##
|
8
|
+
# Pushes new torrents into +TorrentList+.
|
9
|
+
#
|
10
|
+
# @param [Torrent, TorrentList] other
|
11
|
+
#
|
12
|
+
# @exception UnexpectedClass
|
13
|
+
# Raised when +other+ param class is not +Torrent+ or +TorrentList+
|
14
|
+
#
|
15
|
+
# @return [TorrentList<Torrent>]
|
16
|
+
|
17
|
+
def push(other)
|
18
|
+
case other
|
19
|
+
when Torrent
|
20
|
+
@torrents.push other
|
21
|
+
when TorrentList
|
22
|
+
@torrents += other.to_a
|
23
|
+
else
|
24
|
+
raise UnexpectedClass, other
|
25
|
+
end
|
26
|
+
|
27
|
+
return self
|
28
|
+
end
|
29
|
+
alias :<< :push
|
30
|
+
alias :+ :push
|
31
|
+
|
32
|
+
##
|
33
|
+
# Creates new TorrentList instance.
|
34
|
+
#
|
35
|
+
# @return [TorrentList]
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@torrents = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def each(&block) # for Enumerable mixin
|
42
|
+
@torrents.each { |obj| block.call obj }
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# See TorrentList#push description.
|
47
|
+
|
48
|
+
class UnexpectedClass < TTWError
|
49
|
+
def initialize(obj); super "Object #{obj} cannot being pushed into TorrentList, since it's '#{obj.class}' class when only TorrentList/Torrent instances can be pushed"; end end
|
50
|
+
end # class TorrentList
|
51
|
+
end # module TTWatcher
|