ttwatcher 1.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/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
|