torrents 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +5 -0
- data/Gemfile +4 -0
- data/README.md +188 -0
- data/Rakefile +2 -0
- data/lib/torrents.rb +140 -0
- data/lib/torrents/container.rb +246 -0
- data/lib/torrents/trackers/the_pirate_bay.rb +51 -0
- data/lib/torrents/trackers/torrentleech.rb +51 -0
- data/lib/torrents/trackers/tti.rb +51 -0
- data/spec/data/the_pirate_bay/details.html +553 -0
- data/spec/data/the_pirate_bay/movies.html +649 -0
- data/spec/data/the_pirate_bay/recent.html +649 -0
- data/spec/data/the_pirate_bay/search.html +650 -0
- data/spec/data/torrentleech/details.html +218 -0
- data/spec/data/torrentleech/movies.html +2104 -0
- data/spec/data/torrentleech/recent.html +2128 -0
- data/spec/data/torrentleech/search.html +2054 -0
- data/spec/data/tti/details.html +335 -0
- data/spec/data/tti/movies.html +450 -0
- data/spec/data/tti/recent.html +450 -0
- data/spec/data/tti/search.html +451 -0
- data/spec/shared_spec.rb +111 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/torrent_spec.rb +186 -0
- data/spec/torrents_spec.rb +231 -0
- data/spec/trackers/the_pirate_bay_spec.rb +58 -0
- data/spec/trackers/torrentleech_spec.rb +67 -0
- data/spec/trackers/tti_spec.rb +68 -0
- data/torrents.gemspec +31 -0
- metadata +180 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
# Torrents
|
2
|
+
|
3
|
+
Search and download torrents from your favorite bittorrent tracker using Ruby.
|
4
|
+
|
5
|
+
Download and get information like:
|
6
|
+
|
7
|
+
- Subtitles, english and swedish.
|
8
|
+
- Movie information (if the torrent is a movie), actors, grade, original title, length, trailers and so on.
|
9
|
+
- A direct download link to the torrent.
|
10
|
+
- [IMDB](http://imdb.com) link
|
11
|
+
|
12
|
+
## Which trackers are implemented at the moment?
|
13
|
+
|
14
|
+
### Open trackers
|
15
|
+
|
16
|
+
- [The Pirate Bay](http://thepiratebay.org/)
|
17
|
+
|
18
|
+
### Closed trackers
|
19
|
+
|
20
|
+
- [TTI](http://tti.nu/)
|
21
|
+
- [Torrentleech](http://www.torrentleech.org/)
|
22
|
+
|
23
|
+
## How to use
|
24
|
+
|
25
|
+
### Search for torrents
|
26
|
+
|
27
|
+
>> Torrents.the_pirate_bay.search("chuck").results
|
28
|
+
|
29
|
+
### List recent torrents
|
30
|
+
|
31
|
+
>> Torrents.the_pirate_bay.results
|
32
|
+
|
33
|
+
### List recent torrents - with category
|
34
|
+
|
35
|
+
>> Torrents.the_pirate_bay.category(:movies).results
|
36
|
+
|
37
|
+
### Specify a page
|
38
|
+
|
39
|
+
The `page` method can be places anywhere before the `results` method.
|
40
|
+
|
41
|
+
It starts counting from `1` and goes up, no matter what is used on the site it self.
|
42
|
+
|
43
|
+
>> Torrents.the_pirate_bay.page(6).results
|
44
|
+
|
45
|
+
### Specify some cookies
|
46
|
+
|
47
|
+
Some trackers requires cookies to work, even though [The Pirate Bay](http://thepiratebay.org/) is not one of them.
|
48
|
+
|
49
|
+
>> Torrents.the_pirate_bay.cookies(user_id: "123", hash: "c4656002ce46f9b418ce72daccfa5424").results
|
50
|
+
|
51
|
+
## What methods to work with
|
52
|
+
|
53
|
+
### The results method
|
54
|
+
|
55
|
+
As soon as you apply the `results` method on the query it will try to execute your request.
|
56
|
+
If you for example want to activate the debugger, define some cookies or specify a page, then you might do something like this.
|
57
|
+
|
58
|
+
$ Torrents.the_pirate_bay.page(5).debug(true).cookies(:my_cookie => "value").results
|
59
|
+
|
60
|
+
It will return a list of `Container::Torrent` object if the request was sucessfull, otherwise an empty list.
|
61
|
+
|
62
|
+
### The find_by_details method
|
63
|
+
|
64
|
+
If you have access to a single details link and want to get some useful data from it, then `find_by_details` might fit you needs.
|
65
|
+
|
66
|
+
The method takes the url as an argument and returns a single `Container::Torrent` object.
|
67
|
+
|
68
|
+
$ Torrents.the_pirate_bay.find_by_details("http://thepiratebay.org/torrent/6173093/")
|
69
|
+
|
70
|
+
# What data to work with
|
71
|
+
|
72
|
+
### The Container::Torrent class
|
73
|
+
|
74
|
+
The class has some nice accessors that might be useful.
|
75
|
+
|
76
|
+
- **title** (String) The title.
|
77
|
+
- **details** (String) The url to the details page.
|
78
|
+
- **seeders** (Fixnum) The amount of seeders.
|
79
|
+
- **dead?** (Boolean) Check to see if the torrent has no seeders. If it has no seeders, then `dead?` will be true.
|
80
|
+
- **torrent** (String) The url. This should be a direct link to the torrent.
|
81
|
+
- **id** (Fixnum) An unique id for the torrent. The id is only unique for this specific torrent, not all torrents.
|
82
|
+
- **tid** (String) The `tid` method, also known as `torrent id` is a *truly* unique identifier for all torrents. It is generated using a [MD5](http://sv.wikipedia.org/wiki/MD5) with the torrent domain and the `id` method as a seed.
|
83
|
+
- **torrent_id** (String) The same as the `tid` method.
|
84
|
+
- **imdb** (String) The imdb link for the torrent, if the details view contains one.
|
85
|
+
- **imdb_id** (String) The imdb id for the torrent, if the details view contain one. Example: tt0066026.
|
86
|
+
- **subtitle** ([Undertexter](https://github.com/oleander/Undertexter)) The subtitle for the torrent. Takes one argument, the language for the subtitle. Default is `:english`. Read more about it [here](https://github.com/oleander/Undertexter).
|
87
|
+
- **movie** ([MovieSearcher](https://github.com/oleander/MovieSearcher)) Read more about the returned object at the [MovieSearcher](https://github.com/oleander/MovieSearcher) project page.
|
88
|
+
|
89
|
+
**Note:** The `seeders`, `movie`, `subtitle`, `imdb_id` and `ìmdb` method will do another request to the tracker, which means that it will take a bit longer to load then the other methods.
|
90
|
+
|
91
|
+
## What cookies to pass
|
92
|
+
|
93
|
+
Here is an example
|
94
|
+
|
95
|
+
$ Torrents.torrentleech.cookies({:member_id => "123", :pass_hash => "value", :PHPSESSID => "value"}).results
|
96
|
+
|
97
|
+
All values you pass to `cookies` must be of type string, like in the example above.
|
98
|
+
|
99
|
+
- Torrentleech
|
100
|
+
- member_id
|
101
|
+
- pass_hash
|
102
|
+
- PHPSESSID
|
103
|
+
- TTI
|
104
|
+
- hass
|
105
|
+
- pass
|
106
|
+
- uid
|
107
|
+
|
108
|
+
**Note:** The cookies you pass might be browser and IP-adress sensitive. Which means that it might only work in the current browser using the current Internet connection.
|
109
|
+
|
110
|
+
## Error handling
|
111
|
+
|
112
|
+
I decided in the beginning of the project to rescue parse errors during the runtime and instead print them as warnings.
|
113
|
+
|
114
|
+
### Why? - Lack of good selectors
|
115
|
+
|
116
|
+
The trackers parser, [this](https://github.com/oleander/Torrents/blob/master/lib/torrents/trackers/the_pirate_bay.rb) one for example, isn't always returning the right data.
|
117
|
+
|
118
|
+
Due to the lack of useful CSS selectors on the given tracker. It returns 32 rows, the first and the last containing the header and the footer of the table.
|
119
|
+
The unwanted results will be thrown away by the [validator](https://github.com/oleander/Torrents/blob/master/lib/torrents/container.rb#L141), but may raise errors during the run time.
|
120
|
+
The easiest way to solve it was to just isolate the tracker, if it raised an error we return nil.
|
121
|
+
|
122
|
+
### Get the error messages
|
123
|
+
|
124
|
+
You can read errors in two ways.
|
125
|
+
|
126
|
+
Activate the debugger by adding the `debug` method to your query. The errors will be printed as warnings in the console.
|
127
|
+
|
128
|
+
$ Torrents.the_pirate_bay.debug(true).results
|
129
|
+
|
130
|
+
Request a list of errors using the `errors` method.
|
131
|
+
|
132
|
+
$ Torrents.the_pirate_bay.errors
|
133
|
+
>> ["...undefined method `attr' for nil:NilClass>...", "32 torrents where found, 2 where not valid", "..."]
|
134
|
+
|
135
|
+
## How do access tracker X
|
136
|
+
|
137
|
+
Here is how to access an implemented tracker.
|
138
|
+
The first static method to apply is the name of the tracker in lower non camel cased letters.
|
139
|
+
|
140
|
+
The Pirate Bay becomes `the_pirate_bay`, TTI becomes `tti` and Torrentleech `torrentleech`.
|
141
|
+
|
142
|
+
Here is an example.
|
143
|
+
|
144
|
+
$ Torrents.torrentleech.cookies({:my_cookie => "value"}).results
|
145
|
+
|
146
|
+
Take a look at the [tests](https://github.com/oleander/Torrents/tree/master/spec/trackers) for all trackers to get to know more.
|
147
|
+
|
148
|
+
## Add you own tracker
|
149
|
+
|
150
|
+
I'm about to write a wiki that describes how to add you own site.
|
151
|
+
Until then, take a look at the parser for [The Pirate Bay](https://github.com/oleander/Torrents/blob/master/lib/torrents/trackers/the_pirate_bay.rb).
|
152
|
+
|
153
|
+
All heavy lifting has already been done, so adding another tracker should be quite easy.
|
154
|
+
|
155
|
+
I'm using [Nokogiri](http://nokogiri.org/) to parse data from the site, which in most cases means that you don't have to mess with regular expressions.
|
156
|
+
|
157
|
+
Don't know Nokogiri? Take a look at [this](http://railscasts.com/episodes/190-screen-scraping-with-nokogiri) awesome screen cast by [Ryan Bates](https://github.com/ryanb).
|
158
|
+
|
159
|
+
### The short version
|
160
|
+
|
161
|
+
1. Create your own fork of the project.
|
162
|
+
2. Create and implement a tracker file inside the [tracker directory](https://github.com/oleander/Torrents/tree/master/lib/torrents/trackers).
|
163
|
+
3. Add a cached version of the tracker [here](https://github.com/oleander/Torrents/tree/master/spec/data). **Note:** Remember to remove sensitive data from the cache like user name and uid.
|
164
|
+
4. Add tests for it, [here](https://github.com/oleander/Torrents/blob/master/spec/trackers/the_pirate_bay_spec.rb) is a skeleton for the Pirate Bay class to use as a start.
|
165
|
+
5. Add the tracker to the readme.
|
166
|
+
6. Do a pull request, if you want to share you implementation with the world.
|
167
|
+
|
168
|
+
You don't have to take care about exceptions, `Torrents` does that for you.
|
169
|
+
|
170
|
+
## Disclaimer
|
171
|
+
|
172
|
+
Before you use `Torrents` make sure you have permission from the tracker in question to use their data.
|
173
|
+
|
174
|
+
## How do install
|
175
|
+
|
176
|
+
[sudo] gem install torrents
|
177
|
+
|
178
|
+
## How to use it in a rails 3 project
|
179
|
+
|
180
|
+
Add `gem 'torrents'` to your Gemfile and run `bundle`.
|
181
|
+
|
182
|
+
## Requirements
|
183
|
+
|
184
|
+
Torrents is tested in OS X 10.6.6 using Ruby 1.9.2.
|
185
|
+
|
186
|
+
## License
|
187
|
+
|
188
|
+
Torrents is released under the MIT license.
|
data/Rakefile
ADDED
data/lib/torrents.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
$:.push File.expand_path("../../lib/torrents", __FILE__)
|
2
|
+
$:.push File.expand_path("../../lib/torrents/trackers", __FILE__)
|
3
|
+
|
4
|
+
require 'rest_client'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'torrents/container'
|
7
|
+
|
8
|
+
class Torrents < Container::Shared
|
9
|
+
attr_accessor :page
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@torrents = []
|
13
|
+
@errors = []
|
14
|
+
@url = {
|
15
|
+
callback: lambda { |obj|
|
16
|
+
obj.send(:inner_recent_url)
|
17
|
+
},
|
18
|
+
search: {
|
19
|
+
value: ""
|
20
|
+
}
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def exists?(tracker)
|
25
|
+
File.exists?(File.dirname(File.expand_path( __FILE__)) + "/torrents/trackers/" + tracker.to_s + ".rb")
|
26
|
+
end
|
27
|
+
|
28
|
+
def content
|
29
|
+
@content ||= Nokogiri::HTML(self.download(self.url))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set the default page
|
33
|
+
def inner_page
|
34
|
+
((@page ||= 1) - 1 + self.inner_start_page_index).to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def url
|
38
|
+
@url[:callback].call(self).
|
39
|
+
gsub('<SEARCH>', @url[:search][:value]).
|
40
|
+
gsub('<PAGE>', self.inner_page)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Makes this the {tracker} tracker
|
44
|
+
def add(tracker)
|
45
|
+
@tracker = tracker.to_s; self
|
46
|
+
end
|
47
|
+
|
48
|
+
def page(value)
|
49
|
+
@page = value
|
50
|
+
raise ArgumentError.new("To low page value, remember that the first page has the value 1") if self.inner_page.to_i < 0
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def debugger(value)
|
55
|
+
@debug = value; self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set the search value
|
59
|
+
def search(value)
|
60
|
+
@url.merge!(:callback => lambda { |obj|
|
61
|
+
obj.send(:inner_search_url)
|
62
|
+
}, :search => {:value => value}); self
|
63
|
+
end
|
64
|
+
|
65
|
+
def category(cat)
|
66
|
+
@url.merge!(:callback => lambda { |obj|
|
67
|
+
obj.send(:inner_category_url, cat)
|
68
|
+
}); self
|
69
|
+
end
|
70
|
+
|
71
|
+
def cookies(args)
|
72
|
+
@cookies = args; self
|
73
|
+
end
|
74
|
+
|
75
|
+
# If the user is trying to do some funky stuff to the data
|
76
|
+
def method_missing(method, *args, &block)
|
77
|
+
return self.inner_call($1.to_sym, args.first) if method =~ /^inner_(.+)$/
|
78
|
+
super(method, args, block)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.method_missing(method, *args, &block)
|
82
|
+
this = Torrents.new
|
83
|
+
# Raises an exception if the site isn't in the trackers.yaml file
|
84
|
+
raise Exception.new("The site #{method} does not exist") unless this.exists?(method)
|
85
|
+
|
86
|
+
# Yes, I like return :)
|
87
|
+
return this.add(method)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns a Container::Torrent object
|
91
|
+
# {details} (String) The details url for the torrent
|
92
|
+
def find_by_details(details)
|
93
|
+
self.create_torrent({
|
94
|
+
details: details,
|
95
|
+
tracker: @tracker
|
96
|
+
})
|
97
|
+
end
|
98
|
+
|
99
|
+
# Creates a torrent based on the ingoing arguments
|
100
|
+
# Is used by {find_by_details} and the {results} method
|
101
|
+
# Returns a Container::Torrent object
|
102
|
+
# {arguments} (Hash) The params to the Torrent constructor
|
103
|
+
# The debugger and cookie param is passed by default
|
104
|
+
def create_torrent(arguments)
|
105
|
+
arguments.merge!(:debug => @debug) if @debug
|
106
|
+
arguments.merge!(:cookies => @cookies) if @cookies
|
107
|
+
Container::Torrent.new(arguments)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns errors from the application.
|
111
|
+
# Return type: A list of strings
|
112
|
+
def errors
|
113
|
+
self.results; @errors.uniq
|
114
|
+
end
|
115
|
+
|
116
|
+
def results
|
117
|
+
return @torrents if @torrents.any?
|
118
|
+
counter = 0
|
119
|
+
rejected = 0
|
120
|
+
self.inner_torrents(self.content).each do |tr|
|
121
|
+
counter += 1
|
122
|
+
|
123
|
+
torrent = self.create_torrent({
|
124
|
+
details: self.inner_details(tr),
|
125
|
+
torrent: self.inner_torrent(tr),
|
126
|
+
title: self.inner_title(tr).to_s.strip,
|
127
|
+
tracker: @tracker
|
128
|
+
})
|
129
|
+
|
130
|
+
if torrent.valid?
|
131
|
+
@torrents << torrent
|
132
|
+
else
|
133
|
+
rejected += 1
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
@errors << "#{counter} torrents where found, #{rejected} where not valid" unless rejected.zero?
|
138
|
+
return @torrents
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
module Container
|
2
|
+
require "rest_client"
|
3
|
+
require "nokogiri"
|
4
|
+
require 'rchardet19'
|
5
|
+
require "iconv"
|
6
|
+
require "classify"
|
7
|
+
require "digest/md5"
|
8
|
+
require "movie_searcher"
|
9
|
+
require "undertexter"
|
10
|
+
|
11
|
+
# Loads all trackers inside the trackers directory
|
12
|
+
Dir["#{File.dirname(File.expand_path(__FILE__))}/trackers/*.rb"].each {|rb| require "#{rb}"}
|
13
|
+
|
14
|
+
class Shared
|
15
|
+
include Trackers
|
16
|
+
# Downloads the URL, returns an empty string if an error occurred
|
17
|
+
# Here we try to convert the downloaded content to UTF8,
|
18
|
+
# if we"re at least 60% sure that the content that was downloaded actally is was we think
|
19
|
+
# The timeout is set to 10 seconds, after that time, an empty string will be returned
|
20
|
+
# {url} (String) The URL to download
|
21
|
+
def download(url)
|
22
|
+
begin
|
23
|
+
data = RestClient.get self.url_cleaner(url), {:timeout => 10, :cookies => @cookies}
|
24
|
+
cd = CharDet.detect(data)
|
25
|
+
return (cd["confidence"] > 0.6) ? (Iconv.conv(cd["encoding"] + "//IGNORE", "UTF-8", data) rescue data) : data
|
26
|
+
rescue
|
27
|
+
self.error("Something when wrong when trying to fetch #{url}", $!)
|
28
|
+
end
|
29
|
+
|
30
|
+
# The default value, if {RestClient} for some reason craches (like wrong encoding or a timeout)
|
31
|
+
return ""
|
32
|
+
end
|
33
|
+
|
34
|
+
# Prints a nice(er) error to the console if something went wrong
|
35
|
+
# This is only being called when trying to download or when trying to parse a page
|
36
|
+
# {messages} (String) The custom error to the user
|
37
|
+
# {error} (Exception) The actual error that was thrown
|
38
|
+
# TODO: Implement a real logger => http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/classes/Logger.html
|
39
|
+
def error(messages, error = "")
|
40
|
+
messages = messages.class == Array ? messages : [messages]
|
41
|
+
error = error.inspect[0..60]
|
42
|
+
@errors = [] unless @errors
|
43
|
+
@errors << "#{messages.join(", ")}\n#{error}"
|
44
|
+
|
45
|
+
return unless @debug
|
46
|
+
|
47
|
+
warn "An error in the Torrents gem occurred"
|
48
|
+
warn "==> " + messages.join("\n\t")
|
49
|
+
warn "==> " + error + " ..."
|
50
|
+
warn "\n\n"
|
51
|
+
end
|
52
|
+
|
53
|
+
# A middle caller that can handle errors for external trackers
|
54
|
+
# If the tracker that is being loaded in {load} crashes,
|
55
|
+
# then this method makes sure that the entire application won"t crash
|
56
|
+
# {method} (Hash) The method that is being called inside the trackers module
|
57
|
+
# {tr} (Nokogiri | Symbol) The object that contains the HTML content of the current row
|
58
|
+
def inner_call(method, option = nil)
|
59
|
+
begin
|
60
|
+
results = option.nil? ? self.load.send(method) : self.load.send(method, option) if self.valid_option?(method, option)
|
61
|
+
rescue
|
62
|
+
self.error("An error occurred in the #{@tracker} class at the #{method} method.", $!)
|
63
|
+
ensure
|
64
|
+
raise NotImplementedError.new("#{option} is not implemented yet") if results.nil? and method == :category_url
|
65
|
+
value = results.nil? ? self.default_values(method) : results
|
66
|
+
end
|
67
|
+
|
68
|
+
return value
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns default value if any of the below methods (:details for example) return an exception.
|
72
|
+
# If the method for some reason isn't implemented (is not in the hash below), then it will return an empty string
|
73
|
+
# {method} (Hash) The method that raised an exception
|
74
|
+
def default_values(method)
|
75
|
+
# warn "Something went wrong, we can't find the #{method} tag, using default values"
|
76
|
+
{torrent: "", torrents: [], seeders: 1, title: "", details: "", id: 0}[method] || ""
|
77
|
+
end
|
78
|
+
|
79
|
+
# Creating a singleton of the {tracker} class
|
80
|
+
def load
|
81
|
+
@load ||= eval("#{Classify.new.camelize(@tracker)}.new")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check to see if the ingoing arguments to the tracker if valid.
|
85
|
+
# If something goes wrong after the parser has been implemented, then it (the tracker) wont crash.
|
86
|
+
# Insted we write to a log file, so that the user can figure out the problem afterwards.
|
87
|
+
# {method} (Symbol) That method that is being called
|
88
|
+
# {option} (Object) That params to the {method}, can be anything, including {nil}
|
89
|
+
# Returns a boolean, {true} if the {method} can handle the {option} params, {false} otherwise.
|
90
|
+
def valid_option?(method, option)
|
91
|
+
case method
|
92
|
+
when :details, :title, :torrent
|
93
|
+
option.instance_of?(Nokogiri::XML::Element)
|
94
|
+
when :category_url
|
95
|
+
option.instance_of?(Symbol)
|
96
|
+
when :torrents, :seeders
|
97
|
+
option.instance_of?(Nokogiri::HTML::Document)
|
98
|
+
when :id
|
99
|
+
option.instance_of?(String)
|
100
|
+
else
|
101
|
+
true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
# Cleans up the URL
|
105
|
+
# The ingoing param to the {open | RestClient} method can handle the special characters below.
|
106
|
+
# The only way to download the content that the URL points to is to escape those characters.
|
107
|
+
# Read more about it here => http://stackoverflow.com/questions/4999322/escape-and-download-url-using-ruby
|
108
|
+
# {url} (String) The url to escape
|
109
|
+
# Returns an escaped string
|
110
|
+
def url_cleaner(url)
|
111
|
+
url.gsub(/\{|\}|\||\\|\^|\[|\]|\`|\s+/) { |m| CGI::escape(m) }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Torrent < Shared
|
116
|
+
attr_accessor :details
|
117
|
+
|
118
|
+
def initialize(args)
|
119
|
+
args.keys.each { |name| instance_variable_set "@" + name.to_s, args[name] }
|
120
|
+
@errors = [] unless @errors
|
121
|
+
@debug = false unless @debug
|
122
|
+
end
|
123
|
+
|
124
|
+
# Is the torrent dead?
|
125
|
+
# The definition of dead is; no seeders
|
126
|
+
# Returns a boolean
|
127
|
+
def dead?
|
128
|
+
self.seeders <= 0
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns the amount of seeders for the current torrent
|
132
|
+
# If the seeder-tag isn't found, the value one (1) will be returned.
|
133
|
+
# Returns an integer from 0 to inf
|
134
|
+
def seeders
|
135
|
+
@seeders ||= self.inner_call(:seeders, self.content).to_i
|
136
|
+
end
|
137
|
+
|
138
|
+
# Is the torrent valid?
|
139
|
+
# The definition of valid:
|
140
|
+
# Non of the accessors
|
141
|
+
# => is nil
|
142
|
+
# => contains htmltags
|
143
|
+
# => starts or ends with whitespace
|
144
|
+
# It must also stand up to the following requirements
|
145
|
+
# => The details and torrent url must be valid
|
146
|
+
# => The id for the torrent must only contain integers.
|
147
|
+
# Returns {true} or {false}
|
148
|
+
def valid?
|
149
|
+
[:details, :torrent, :title, :id].each do |method|
|
150
|
+
data = self.send(method)
|
151
|
+
return false if self.send(method).nil? or
|
152
|
+
data.to_s.empty? or
|
153
|
+
data.to_s.match(/<\/?[^>]*>/) or
|
154
|
+
data.to_s.strip != data.to_s
|
155
|
+
end
|
156
|
+
|
157
|
+
return [
|
158
|
+
!! self.valid_url?(self.details),
|
159
|
+
!! self.valid_torrent?(self.torrent),
|
160
|
+
!! self.inner_call(:id, self.details).to_s.match(/^\d+$/)
|
161
|
+
].all?
|
162
|
+
end
|
163
|
+
|
164
|
+
# Downloads the detailed view for this torrent
|
165
|
+
# Returns an Nokogiri object
|
166
|
+
def content
|
167
|
+
@content ||= Nokogiri::HTML self.download(@details)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Check to see if the ingoing param is a valid url or not
|
171
|
+
def valid_url?(url)
|
172
|
+
!! url.match(/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/i)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Check to see if the ingoing param is a valid torrent url or not
|
176
|
+
# The url has to be a valid url and has to end with .torrent
|
177
|
+
def valid_torrent?(torrent)
|
178
|
+
torrent.match(/\.torrent$/) and self.valid_url?(torrent)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Generates an id using the details url
|
182
|
+
def id
|
183
|
+
@id ||= self.inner_call(:id, self.details).to_i
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns the domain for the torrent, without http or www
|
187
|
+
# If the domain for some reason isn't found, it will use an empty string
|
188
|
+
def domain
|
189
|
+
@domain ||= self.details.match(/(ftp|http|https):\/\/([w]+\.)?(.+?\.[a-z]{2,3})/i).to_a[3] || ""
|
190
|
+
end
|
191
|
+
|
192
|
+
# Returns a unique id for the torrent based on the domain and the id of the torrent
|
193
|
+
def tid
|
194
|
+
@tid ||= Digest::MD5.hexdigest("#{domain}#{id}")
|
195
|
+
end
|
196
|
+
|
197
|
+
# Just a mirror method for {tid}, just in case someone don't like the method name tid
|
198
|
+
def torrent_id
|
199
|
+
@torrent_id ||= self.tid
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns the full url to the related imdb page
|
203
|
+
# The link is parsed from the details view
|
204
|
+
# Example: http://www.imdb.com/title/tt0066026
|
205
|
+
# Return type: String or nil
|
206
|
+
def imdb
|
207
|
+
@imdb ||= self.content.to_s.match(/((http:\/\/)?([w]{3}\.)?imdb.com\/title\/tt\d+)/i).to_a[1]
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns the imdb id for the torrent, including the tt at the beginning
|
211
|
+
# Example: tt0066026
|
212
|
+
# Return type: String or nil
|
213
|
+
def imdb_id
|
214
|
+
@imdb_id ||= self.imdb.to_s.match(/(tt\d+)/).to_a[1]
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns an movie_searcher object based on the imdb_id, if it exists, otherwise the torrent title
|
218
|
+
# Read more about it here: https://github.com/oleander/MovieSearcher
|
219
|
+
# Return type: A MovieSearcher object or nil
|
220
|
+
def movie
|
221
|
+
self.imdb_id.nil? ? MovieSearcher.find_by_release_name(self.title, :options => {:details => true}) : MovieSearcher.find_movie_by_id(self.imdb_id)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Returns the title for the torrent
|
225
|
+
# If the title has't been set from the Torrents class, we will download the details page try to find it there.
|
226
|
+
# Return type: String or nil
|
227
|
+
def title
|
228
|
+
@title ||= self.inner_call(:details_title, self.content)
|
229
|
+
@title = @title.strip unless @title.nil?
|
230
|
+
end
|
231
|
+
|
232
|
+
# Returns a Undertexter object, if we found a imdb_id, otherwise nil
|
233
|
+
# Read more about it here: https://github.com/oleander/Undertexter
|
234
|
+
# Return type: A single Undertexter object or nil
|
235
|
+
def subtitle(option = :english)
|
236
|
+
@subtitle ||= Undertexter.find(self.imdb_id, language: option).based_on(self.title)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns the torrent for the torrent
|
240
|
+
# If the torrent has't been set from the Torrents class, we will download the details page try to find it there.
|
241
|
+
# Return type: String or nil
|
242
|
+
def torrent
|
243
|
+
@torrent ||= self.inner_call(:details_torrent, self.content)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|