torrents 1.0.0
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.
- 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
|