download_tv 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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/README.md +50 -0
- data/Rakefile +17 -0
- data/bin/tv +56 -0
- data/download_tv.gemspec +36 -0
- data/lib/download_tv.rb +12 -0
- data/lib/download_tv/config_example.rb +12 -0
- data/lib/download_tv/downloader.rb +125 -0
- data/lib/download_tv/grabbers/addic7ed.rb +67 -0
- data/lib/download_tv/grabbers/eztv.rb +30 -0
- data/lib/download_tv/grabbers/torrentapi.rb +65 -0
- data/lib/download_tv/grabbers/tpb.rb +34 -0
- data/lib/download_tv/linkgrabber.rb +32 -0
- data/lib/download_tv/myepisodes.rb +87 -0
- data/lib/download_tv/subtitles.rb +20 -0
- data/lib/download_tv/torrent.rb +123 -0
- data/lib/download_tv/version.rb +3 -0
- data/test/downloader_test.rb +64 -0
- data/test/grabbers_test.rb +53 -0
- data/test/test_helper.rb +5 -0
- data/test/torrent_test.rb +33 -0
- metadata +170 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 16bd7893a980f0e82a8679f7c3325e9b31e91841
|
4
|
+
data.tar.gz: 7ed719b758086d4f0bc5811fc27f8e212dd592f7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c4354d813ded87ee149c79cd99112a1a08d5abf86a52e8e0b04f32445186c2806454412f3cd52bd32e9587d79bff3e48a941aaf8426bace28676466e2795e2bd
|
7
|
+
data.tar.gz: f6e4ae1b8e80a3f9c0f7be4f8a19ec0e7426b9470caf8cc006d6eb14fca7e9d164eac42c5a4599ad95f023281144c48a657daba0d0b19f2f73084a78fd0682ac
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# download_tv
|
2
|
+
|
3
|
+
[](https://travis-ci.org/guille/daily-shows)
|
4
|
+
|
5
|
+
download_tv is a Ruby command line application that automatically downloads the new episodes from the shows you follow. It grabs the list of shows from your MyEpisodes account.
|
6
|
+
|
7
|
+
### Installation
|
8
|
+
|
9
|
+
Clone the repository.
|
10
|
+
|
11
|
+
Rename the config_example.rb to config.rb and modify it if needed.
|
12
|
+
|
13
|
+
### Usage
|
14
|
+
|
15
|
+
A binary is provided in /bin/tv.
|
16
|
+
|
17
|
+
```
|
18
|
+
Usage: tv [options]
|
19
|
+
|
20
|
+
Specific options:
|
21
|
+
-o, --offset OFFSET Move back the last run offset
|
22
|
+
-f, --file PATH Download shows from a file
|
23
|
+
-d, --download SHOW Downloads given show
|
24
|
+
--dry-run Don't write to the date file
|
25
|
+
-h, --help Show this message
|
26
|
+
```
|
27
|
+
|
28
|
+
Three actions are recognised:
|
29
|
+
|
30
|
+
* By default, it fetches the list of episodes from MyEpisodes that have aired since the program was run for the last time and tries to download them. The -o flag can be used in order to re-download the episodes from previous days.
|
31
|
+
|
32
|
+
* In order to download a single episode, use the -d flag. Example: *tv -d Breaking Bad S04E01*
|
33
|
+
|
34
|
+
* Finally, the -f flag can be used to download a set of episodes. This option takes a text file as an argument. Each line of the file is interpreted as a episode to download. Example: *tv -f /path/to/listofeps*
|
35
|
+
|
36
|
+
### Configuration
|
37
|
+
|
38
|
+
* myepisodes_user: String containing the username that will be used to log in to MyEpisodes. Set to an empty string to have the application ask for it in each execution.
|
39
|
+
|
40
|
+
* auto: Boolean value (true/false). Determines whether the application will try to automatically select a torrent using pre-determined filters or show the list to the user and let him choose.
|
41
|
+
|
42
|
+
* subs: Not implemented yet. Boolean value (true/false). Determines whether the application will try to find subtitles for the shows being downloaded.
|
43
|
+
|
44
|
+
* cookie_path: String containing a path to where the session cookie for MyEpisodes will be stored. Set it to "" to prevent the cookie from being stored.
|
45
|
+
|
46
|
+
* ignored: Array containing names of TV shows you follow in MyEpisodes but don't want to download. The strings should match the name of the show as displayed by MyEpisodes. Example: ["Boring Show 1", "Boring Show 2"],
|
47
|
+
|
48
|
+
* tpb_proxy: Base URL of the ThePirateBay proxy to use.
|
49
|
+
|
50
|
+
* grabbers: String containing names of the sources where to look for torrents in ascending order of preference. Useful for activating or deactivating specific sites, reordering them or for plugin developers.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs << "lib"
|
8
|
+
t.libs << "test"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
|
+
t.verbose = false
|
11
|
+
end
|
12
|
+
|
13
|
+
task :clean do
|
14
|
+
rm_rf "config.rb"
|
15
|
+
rm_rf "cookie"
|
16
|
+
rm_rf "date"
|
17
|
+
end
|
data/bin/tv
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'download_tv'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
options[:offset] = 0
|
8
|
+
options[:dry] = false
|
9
|
+
options[:cmd] = "run"
|
10
|
+
|
11
|
+
opt_parser = OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: tv [options]"
|
13
|
+
|
14
|
+
opts.separator ""
|
15
|
+
opts.separator "Specific options:"
|
16
|
+
|
17
|
+
opts.on("-o", "--offset OFFSET", Integer, "Move back the last run offset") do |o|
|
18
|
+
options[:offset] = o
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("-f", "--file PATH", "Download shows from a file") do |f|
|
22
|
+
options[:cmd] = "file"
|
23
|
+
options[:arg] = f
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("-d", "--download SHOW", "Downloads given show") do |s|
|
27
|
+
options[:cmd] = "dl"
|
28
|
+
options[:arg] = s
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("--dry-run", "Don't write to the date file") do |n|
|
32
|
+
options[:dry] = n
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
36
|
+
puts opts
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
opt_parser.parse!(ARGV)
|
42
|
+
|
43
|
+
begin
|
44
|
+
dl = DownloadTV::Downloader.new(options[:offset])
|
45
|
+
case options[:cmd]
|
46
|
+
when "run"
|
47
|
+
dl.run(options[:dry])
|
48
|
+
when "dl"
|
49
|
+
dl.download_single_show(options[:arg])
|
50
|
+
when "file"
|
51
|
+
dl.download_from_file(options[:arg])
|
52
|
+
end
|
53
|
+
rescue Interrupt
|
54
|
+
puts "Interrupt signal detected. Exiting..."
|
55
|
+
exit 1
|
56
|
+
end
|
data/download_tv.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "download_tv/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "download_tv"
|
8
|
+
s.version = DownloadTV::VERSION
|
9
|
+
s.authors = ["guille"]
|
10
|
+
s.email = ["guillerg96@gmail.com"]
|
11
|
+
|
12
|
+
s.summary = %q{DownloadTV finds the next episodes from shows you follow on MyEpisodes and downloads them.}
|
13
|
+
s.homepage = "https://github.com/guille/download_tv"
|
14
|
+
|
15
|
+
s.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test)/})
|
17
|
+
end
|
18
|
+
# s.files = `git ls-files -- lib/*`.split($/)
|
19
|
+
s.test_files = `git ls-files -- test/*`.split($/)
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
# s.bindir = "exe"
|
23
|
+
s.executables = ["tv"]
|
24
|
+
s.default_executable = 'tv'
|
25
|
+
|
26
|
+
s.add_development_dependency "bundler", "~> 1.15"
|
27
|
+
s.add_development_dependency "rake", "~> 10.0"
|
28
|
+
s.add_development_dependency "minitest", "~> 5.0"
|
29
|
+
|
30
|
+
s.add_dependency("json")
|
31
|
+
s.add_dependency("mechanize")
|
32
|
+
s.add_dependency("date")
|
33
|
+
s.add_dependency("io-console")
|
34
|
+
|
35
|
+
s.has_rdoc = false
|
36
|
+
end
|
data/lib/download_tv.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "json"
|
2
|
+
require "mechanize"
|
3
|
+
require "date"
|
4
|
+
require "io/console"
|
5
|
+
|
6
|
+
require "download_tv/version"
|
7
|
+
require "download_tv/downloader"
|
8
|
+
require "download_tv/torrent"
|
9
|
+
require "download_tv/myepisodes"
|
10
|
+
require "download_tv/linkgrabber"
|
11
|
+
require "download_tv/subtitles"
|
12
|
+
Dir[File.join(__dir__, 'download_tv', 'grabbers', '*.rb')].each {|file| require file }
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module DownloadTV
|
2
|
+
CONFIG = {
|
3
|
+
myepisodes_user: "", # MyEpisodes login username
|
4
|
+
auto: true, # Try to automatically select the torrents
|
5
|
+
subs: true, # Download subtitles (not implemented yet)
|
6
|
+
cookie_path: "cookie", # Leave blank to prevent the app from storing cookies
|
7
|
+
ignored: [], # list of strings that match show names as written in myepisodes
|
8
|
+
tpb_proxy: "https://pirateproxy.cc", # URL of the TPB proxy to use
|
9
|
+
grabbers: ["Eztv", "ThePirateBay", "TorrentAPI"], # names of the classes in /grabbers
|
10
|
+
}
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
begin
|
2
|
+
require_relative 'config'
|
3
|
+
rescue LoadError
|
4
|
+
puts "Config file not found. Try renaming the config_example file to config.rb"
|
5
|
+
exit
|
6
|
+
end
|
7
|
+
|
8
|
+
module DownloadTV
|
9
|
+
|
10
|
+
class Downloader
|
11
|
+
|
12
|
+
attr_reader :offset, :auto, :subs
|
13
|
+
|
14
|
+
def initialize(offset)
|
15
|
+
@offset = offset.abs
|
16
|
+
@auto = DownloadTV::CONFIG[:auto]
|
17
|
+
# @subs = DownloadTV::CONFIG[:subs]
|
18
|
+
Thread.abort_on_exception = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def download_single_show(show)
|
22
|
+
t = Torrent.new
|
23
|
+
download(t.get_link(show, @auto))
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def download_from_file(filename)
|
28
|
+
raise "File doesn't exist" if !File.exists? filename
|
29
|
+
t = Torrent.new
|
30
|
+
File.readlines(filename).each { |show| download(t.get_link(show, @auto)) }
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Gets the links.
|
36
|
+
def run(dont_write_to_date_file)
|
37
|
+
# Change to installation directory
|
38
|
+
Dir.chdir(__dir__)
|
39
|
+
|
40
|
+
date = check_date
|
41
|
+
|
42
|
+
myepisodes = MyEpisodes.new(DownloadTV::CONFIG[:myepisodes_user], DownloadTV::CONFIG[:cookie_path])
|
43
|
+
# Log in using cookie by default
|
44
|
+
myepisodes.load_cookie
|
45
|
+
shows = myepisodes.get_shows(date)
|
46
|
+
|
47
|
+
if shows.empty?
|
48
|
+
puts "Nothing to download"
|
49
|
+
|
50
|
+
else
|
51
|
+
t = Torrent.new
|
52
|
+
to_download = fix_names(shows)
|
53
|
+
|
54
|
+
queue = Queue.new
|
55
|
+
|
56
|
+
# Adds a link (or empty string to the queue)
|
57
|
+
link_t = Thread.new do
|
58
|
+
to_download.each { |show| queue << t.get_link(show, @auto) }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Downloads the links as they are added
|
62
|
+
download_t = Thread.new do
|
63
|
+
to_download.size.times do
|
64
|
+
magnet = queue.pop
|
65
|
+
next if magnet == "" # Doesn't download if no torrents are found
|
66
|
+
download(magnet)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Downloading the subtitles
|
71
|
+
# subs_t = @subs and Thread.new do
|
72
|
+
# to_download.each { |show| @s.get_subs(show) }
|
73
|
+
# end
|
74
|
+
|
75
|
+
link_t.join
|
76
|
+
download_t.join
|
77
|
+
# subs_t.join
|
78
|
+
|
79
|
+
puts "Completed. Exiting..."
|
80
|
+
end
|
81
|
+
|
82
|
+
File.write("date", Date.today) unless dont_write_to_date_file
|
83
|
+
|
84
|
+
rescue InvalidLoginError
|
85
|
+
puts "Wrong username/password combination"
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def check_date
|
90
|
+
content = File.read("date")
|
91
|
+
|
92
|
+
last = Date.parse(content)
|
93
|
+
if last - @offset != Date.today
|
94
|
+
last - @offset
|
95
|
+
else
|
96
|
+
puts "Everything up to date"
|
97
|
+
exit
|
98
|
+
end
|
99
|
+
|
100
|
+
rescue Errno::ENOENT
|
101
|
+
File.write("date", Date.today-1)
|
102
|
+
retry
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def fix_names(shows)
|
107
|
+
# Ignored shows
|
108
|
+
s = shows.reject do |i|
|
109
|
+
# Remove season+episode
|
110
|
+
DownloadTV::CONFIG[:ignored].include?(i.split(" ")[0..-2].join(" "))
|
111
|
+
end
|
112
|
+
|
113
|
+
# Removes apostrophes and parens
|
114
|
+
s.map { |t| t.gsub(/ \(.+\)|[']/, "") }
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def download(link)
|
119
|
+
exec = "xdg-open \"#{link}\""
|
120
|
+
|
121
|
+
Process.detach(Process.spawn(exec, [:out, :err]=>"/dev/null"))
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module DownloadTV
|
2
|
+
|
3
|
+
class Addic7ed < LinkGrabber
|
4
|
+
def initialize
|
5
|
+
super("http://www.addic7ed.com/search.php?search=%s&Submit=Search", "+")
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_subs(show)
|
9
|
+
url = get_url(show)
|
10
|
+
download_file(url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_url(show)
|
14
|
+
# Change spaces for the separator
|
15
|
+
s = show.gsub(" ", @sep)
|
16
|
+
|
17
|
+
# Format the url
|
18
|
+
search = @url % [s]
|
19
|
+
|
20
|
+
agent = Mechanize.new
|
21
|
+
res = agent.get(search)
|
22
|
+
|
23
|
+
# No redirection means no subtitle found
|
24
|
+
raise NoSubtitlesError if res.uri.to_s == search
|
25
|
+
|
26
|
+
##########
|
27
|
+
# DO OPENSUBTITLES FIRST (see subtitles.rb)
|
28
|
+
#####
|
29
|
+
|
30
|
+
# We now have an URL like:
|
31
|
+
# http://www.addic7ed.com/serie/Mr._Robot/2/3/eps2.1k3rnel-pan1c.ksd
|
32
|
+
|
33
|
+
# To find the real links:
|
34
|
+
# see comments at the end of file
|
35
|
+
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def download_file(url)
|
40
|
+
# Url must be like "http://www.addic7ed.com/updated/1/115337/0"
|
41
|
+
|
42
|
+
# ADDIC7ED PROVIDES RSS
|
43
|
+
|
44
|
+
agent = Mechanize.new
|
45
|
+
page = agent.get(url2, [], @url)
|
46
|
+
puts page.save("Hi")
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# subtitles = {}
|
55
|
+
# html.css(".tabel95 .newsDate").each do |td|
|
56
|
+
# if downloads = td.text.match(/\s(\d*)\sDownloads/i)
|
57
|
+
# done = false
|
58
|
+
# td.parent.parent.xpath("./tr/td/a[@class='buttonDownload']/@href").each do |link|
|
59
|
+
# if md = link.value.match(/updated/i)
|
60
|
+
# subtitles[downloads[1].to_i] = link.value
|
61
|
+
# done = true
|
62
|
+
# elsif link.value.match(/original/i) && done == false
|
63
|
+
# subtitles[downloads[1].to_i] = link.value
|
64
|
+
# done = true
|
65
|
+
# end
|
66
|
+
# end
|
67
|
+
# end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module DownloadTV
|
2
|
+
class Eztv < LinkGrabber
|
3
|
+
def initialize
|
4
|
+
super("https://eztv.ag/search/%s")
|
5
|
+
end
|
6
|
+
|
7
|
+
def get_links(s)
|
8
|
+
|
9
|
+
# Format the url
|
10
|
+
search = @url % [s]
|
11
|
+
|
12
|
+
data = @agent.get(search).search("a.magnet")
|
13
|
+
|
14
|
+
# Torrent name in data[i].attribute "title"
|
15
|
+
# "Suits S04E01 HDTV x264-LOL Torrent: Magnet Link"
|
16
|
+
|
17
|
+
# EZTV shows 50 latest releases if it can't find the torrent
|
18
|
+
raise NoTorrentsError if data.size == 50
|
19
|
+
|
20
|
+
names = data.collect { |i| i.attribute("title").text.chomp(" Magnet Link") }
|
21
|
+
links = data.collect { |i| i.attribute("href").text }
|
22
|
+
|
23
|
+
names.zip(links)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module DownloadTV
|
2
|
+
|
3
|
+
class TorrentAPI < LinkGrabber
|
4
|
+
|
5
|
+
attr_accessor :token
|
6
|
+
attr_reader :wait
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super("https://torrentapi.org/pubapi_v2.php?mode=search&search_string=%s&token=%s")
|
10
|
+
@token = get_token
|
11
|
+
@wait = 2.1
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Connects to Torrentapi.org and requests a token.
|
17
|
+
# Returns said token.
|
18
|
+
def get_token
|
19
|
+
page = @agent.get("https://torrentapi.org/pubapi_v2.php?get_token=get_token").content
|
20
|
+
|
21
|
+
obj = JSON.parse(page)
|
22
|
+
|
23
|
+
@token = obj['token']
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_links(s)
|
28
|
+
|
29
|
+
# Format the url
|
30
|
+
search = @url % [s, @token]
|
31
|
+
|
32
|
+
page = @agent.get(search).content
|
33
|
+
obj = JSON.parse(page)
|
34
|
+
|
35
|
+
if obj["error_code"]==4 # Token expired
|
36
|
+
get_token
|
37
|
+
search = @url % [s, @token]
|
38
|
+
page = @agent.get(search).content
|
39
|
+
obj = JSON.parse(page)
|
40
|
+
end
|
41
|
+
|
42
|
+
while obj["error_code"]==5 # Violate 1req/2s limit
|
43
|
+
# puts "Torrentapi request limit hit. Wait a few seconds..."
|
44
|
+
sleep(@wait)
|
45
|
+
page = @agent.get(search).content
|
46
|
+
obj = JSON.parse(page)
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
raise NoTorrentsError if obj["error"]
|
51
|
+
|
52
|
+
names = obj["torrent_results"].collect {|i| i["filename"]}
|
53
|
+
links = obj["torrent_results"].collect {|i| i["download"]}
|
54
|
+
|
55
|
+
names.zip(links)
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# Tokens automaticly expire in 15 minutes.
|
64
|
+
# The api has a 1req/2s limit.
|
65
|
+
# http://torrentapi.org/apidocs_v2.txt
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module DownloadTV
|
2
|
+
|
3
|
+
class ThePirateBay < LinkGrabber
|
4
|
+
|
5
|
+
def initialize()
|
6
|
+
proxy = DownloadTV::CONFIG[:tpb_proxy].gsub(/\/+$/, "") || "https://thepiratebay.cr"
|
7
|
+
|
8
|
+
super("#{proxy}/search/%s/0/7/0")
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_links(s)
|
13
|
+
|
14
|
+
# Format the url
|
15
|
+
search = @url % [s]
|
16
|
+
|
17
|
+
data = @agent.get(search).search("#searchResult tr")
|
18
|
+
# Skip the header
|
19
|
+
data = data.drop 1
|
20
|
+
|
21
|
+
raise NoTorrentsError if data.size == 0
|
22
|
+
|
23
|
+
# Second cell of each row contains links and name
|
24
|
+
results = data.map { |d| d.search("td")[1] }
|
25
|
+
|
26
|
+
names = results.collect {|i| i.search(".detName").text.strip }
|
27
|
+
links = results.collect {|i| i.search("a")[1].attribute("href").text }
|
28
|
+
|
29
|
+
names.zip(links)
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module DownloadTV
|
2
|
+
|
3
|
+
class LinkGrabber
|
4
|
+
attr_reader :url
|
5
|
+
|
6
|
+
def initialize(url)
|
7
|
+
@url = url
|
8
|
+
@agent = Mechanize.new
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_connection
|
13
|
+
agent = Mechanize.new
|
14
|
+
agent.read_timeout = 2
|
15
|
+
agent.get(@url)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_links(s)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
class NoTorrentsError < StandardError
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class NoSubtitlesError < StandardError
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module DownloadTV
|
2
|
+
|
3
|
+
class MyEpisodes
|
4
|
+
|
5
|
+
def initialize(user=nil, cookie_path="")
|
6
|
+
@agent = Mechanize.new
|
7
|
+
@user = user
|
8
|
+
@cookie_path = cookie_path
|
9
|
+
@save_cookie = cookie_path != ""
|
10
|
+
end
|
11
|
+
|
12
|
+
def login
|
13
|
+
if !@user || @user==""
|
14
|
+
print "Enter your MyEpisodes username: "
|
15
|
+
@user = STDIN.gets.chomp
|
16
|
+
end
|
17
|
+
|
18
|
+
print "Enter your MyEpisodes password: "
|
19
|
+
pass = STDIN.noecho(&:gets).chomp
|
20
|
+
puts
|
21
|
+
|
22
|
+
page = @agent.get "https://www.myepisodes.com/login.php"
|
23
|
+
|
24
|
+
login_form = page.forms[1]
|
25
|
+
login_form.username = @user
|
26
|
+
login_form.password = pass
|
27
|
+
|
28
|
+
page = @agent.submit(login_form, login_form.buttons.first)
|
29
|
+
|
30
|
+
raise InvalidLoginError if page.filename == "login.php"
|
31
|
+
|
32
|
+
save_cookie() if @save_cookie
|
33
|
+
|
34
|
+
@agent
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_cookie
|
39
|
+
if File.exists? @cookie_path
|
40
|
+
@agent.cookie_jar.load @cookie_path
|
41
|
+
page = @agent.get "https://www.myepisodes.com/login.php"
|
42
|
+
if page.links[1].text == "Register"
|
43
|
+
puts "The cookie is invalid/has expired."
|
44
|
+
login
|
45
|
+
end
|
46
|
+
@agent
|
47
|
+
else
|
48
|
+
puts "Cookie file not found"
|
49
|
+
login
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def save_cookie
|
55
|
+
@agent.cookie_jar.save(@cookie_path, session: true)
|
56
|
+
@agent
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_shows(last)
|
61
|
+
page = @agent.get "https://www.myepisodes.com/ajax/service.php?mode=view_privatelist"
|
62
|
+
shows = page.parser.css('tr.past')
|
63
|
+
|
64
|
+
s = shows.select do |i|
|
65
|
+
airdate = i.css('td.date')[0].text
|
66
|
+
Date.parse(airdate) >= last
|
67
|
+
end
|
68
|
+
|
69
|
+
s.map do |i|
|
70
|
+
name = i.css('td.showname').text
|
71
|
+
ep = i.css('td.longnumber').text
|
72
|
+
|
73
|
+
ep.insert(0, "S")
|
74
|
+
ep.sub!("x", "E")
|
75
|
+
|
76
|
+
"#{name} #{ep}"
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class InvalidLoginError < StandardError
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module DownloadTV
|
2
|
+
|
3
|
+
class Torrent
|
4
|
+
|
5
|
+
attr_reader :g_names, :g_instances, :n_grabbers
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@g_names = DownloadTV::CONFIG[:grabbers].clone
|
9
|
+
@g_instances = Array.new
|
10
|
+
@n_grabbers = @g_names.size # Initial size
|
11
|
+
@tries = @n_grabbers - 1
|
12
|
+
|
13
|
+
@filters = [
|
14
|
+
->(n){n.include?("2160")},
|
15
|
+
->(n){n.include?("1080")},
|
16
|
+
->(n){n.include?("720")},
|
17
|
+
->(n){n.include?("WEB")},
|
18
|
+
->(n){!n.include?("PROPER") || !n.include?("REPACK")},
|
19
|
+
]
|
20
|
+
|
21
|
+
change_grabbers
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def change_grabbers
|
27
|
+
if !@g_names.empty?
|
28
|
+
# Instantiates the last element from g_names, popping it
|
29
|
+
newt = (DownloadTV.const_get @g_names.pop).new
|
30
|
+
newt.test_connection
|
31
|
+
|
32
|
+
@g_instances.unshift newt
|
33
|
+
|
34
|
+
else
|
35
|
+
# Rotates the instantiated grabbers
|
36
|
+
@g_instances.rotate!
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
rescue Mechanize::ResponseCodeError, Net::HTTP::Persistent::Error
|
41
|
+
|
42
|
+
puts "Problem accessing #{newt.class.name}"
|
43
|
+
# We won't be using this grabber
|
44
|
+
@n_grabbers = @n_grabbers-1
|
45
|
+
@tries = @tries - 1
|
46
|
+
|
47
|
+
change_grabbers
|
48
|
+
|
49
|
+
rescue SocketError, Errno::ECONNRESET, Net::OpenTimeout
|
50
|
+
puts "Connection error."
|
51
|
+
exit
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def get_link(show, auto)
|
57
|
+
links = @g_instances.first.get_links(show)
|
58
|
+
|
59
|
+
if !auto
|
60
|
+
links.each_with_index do |data, i|
|
61
|
+
puts "#{i}\t\t#{data[0]}"
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
puts
|
66
|
+
print "Select the torrent you want to download [-1 to skip]: "
|
67
|
+
|
68
|
+
i = $stdin.gets.chomp.to_i
|
69
|
+
|
70
|
+
while i >= links.size || i < -1
|
71
|
+
puts "Index out of bounds. Try again: "
|
72
|
+
i = $stdin.gets.chomp.to_i
|
73
|
+
end
|
74
|
+
|
75
|
+
# Reset the counter
|
76
|
+
@tries = @n_grabbers - 1
|
77
|
+
|
78
|
+
# Use -1 to skip the download
|
79
|
+
i == -1 ? "" : links[i][1]
|
80
|
+
|
81
|
+
else # Automatically get the links
|
82
|
+
|
83
|
+
links = filter_shows(links)
|
84
|
+
|
85
|
+
# Reset the counter
|
86
|
+
@tries = @n_grabbers - 1
|
87
|
+
|
88
|
+
# Get the first result left
|
89
|
+
links[0][1]
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
rescue NoTorrentsError
|
94
|
+
puts "No torrents found for #{show} using #{@g_instances.first.class.name}"
|
95
|
+
|
96
|
+
# Use next grabber
|
97
|
+
if @tries > 0
|
98
|
+
@tries-=1
|
99
|
+
change_grabbers
|
100
|
+
retry
|
101
|
+
|
102
|
+
else # Reset the counter
|
103
|
+
@tries = @n_grabbers - 1
|
104
|
+
# TODO: Handle show not found here!!
|
105
|
+
return ""
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
def filter_shows(links)
|
112
|
+
@filters.each do |f| # Apply each filter
|
113
|
+
new_links = links.reject {|name, link| f.(name)}
|
114
|
+
# Stop if the filter removes every release
|
115
|
+
break if new_links.size == 0
|
116
|
+
|
117
|
+
links = new_links
|
118
|
+
end
|
119
|
+
links
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
describe DownloadTV::Downloader do
|
4
|
+
before do
|
5
|
+
Dir.chdir(File.dirname(__FILE__))
|
6
|
+
File.delete("date") if File.exist?("date")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "when creating the object" do
|
10
|
+
it "should receive an integer" do
|
11
|
+
->{ DownloadTV::Downloader.new("foo") }.must_raise NoMethodError
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should store the first argument as @offset" do
|
15
|
+
DownloadTV::Downloader.new(3).offset.must_equal 3
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "the fix_names function" do
|
21
|
+
it "should remove apostrophes and parens" do
|
22
|
+
shows = ["Mr. Foo S01E02", "Bar (UK) S00E22", "Let's S05E03"]
|
23
|
+
result = ["Mr. Foo S01E02", "Bar S00E22", "Lets S05E03"]
|
24
|
+
DownloadTV::Downloader.new(0).fix_names(shows).must_equal result
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should remove ignored shows" do
|
28
|
+
DownloadTV::CONFIG[:ignored] = ["Ignored"]
|
29
|
+
shows = ["Mr. Foo S01E02", "Bar (UK) S00E22", "Ignored S20E22", "Let's S05E03"]
|
30
|
+
result = ["Mr. Foo S01E02", "Bar S00E22", "Lets S05E03"]
|
31
|
+
DownloadTV::Downloader.new(0).fix_names(shows).must_equal result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
describe "the date file" do
|
37
|
+
dl = DownloadTV::Downloader.new(0)
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
it "should be created if it doesn't exist" do
|
42
|
+
dl.check_date
|
43
|
+
File.exist?("date").must_equal true
|
44
|
+
end
|
45
|
+
|
46
|
+
it "contains a date after running the method" do
|
47
|
+
date = dl.check_date
|
48
|
+
date.must_equal (Date.today-1)
|
49
|
+
Date.parse(File.read("date")).must_equal Date.today-1
|
50
|
+
end
|
51
|
+
|
52
|
+
it "exits the script when up to date" do
|
53
|
+
File.write("date", Date.today)
|
54
|
+
begin
|
55
|
+
dl.check_date
|
56
|
+
flunk
|
57
|
+
rescue SystemExit
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
describe DownloadTV::LinkGrabber do
|
4
|
+
|
5
|
+
grabbers = DownloadTV::CONFIG[:grabbers].clone
|
6
|
+
instances = grabbers.map { |g| (DownloadTV.const_get g).new }
|
7
|
+
|
8
|
+
instances.each do |grabber|
|
9
|
+
describe grabber do
|
10
|
+
# grabber = g#(Object.const_get "DownloadTV::#{g}").new
|
11
|
+
|
12
|
+
it "will have a url attribute on creation" do
|
13
|
+
# instance_eval("#{g}.new")
|
14
|
+
|
15
|
+
grabber.url.wont_be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should get a 200 code response" do
|
19
|
+
# grabber = (Object.const_get g).new
|
20
|
+
grabber.test_connection.code.must_equal "200"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "will raise NoTorrentsError when torrent can't be found" do
|
24
|
+
# grabber = (Object.const_get g).new
|
25
|
+
notfound = ->{ grabber.get_links("Totally Fake Show askjdgsaudas") }
|
26
|
+
notfound.must_raise DownloadTV::NoTorrentsError
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
it "will return an array with names and links of results when a torrent can be found" do
|
31
|
+
# grabber = (Object.const_get g).new
|
32
|
+
result = grabber.get_links("Game Of Thrones S04E01")
|
33
|
+
result.must_be_instance_of Array
|
34
|
+
result.wont_be :empty?
|
35
|
+
result.each do |r|
|
36
|
+
r.size.must_equal 2
|
37
|
+
r[0].must_be_instance_of String
|
38
|
+
r[0].upcase.must_include "THRONES"
|
39
|
+
r[1].must_be_instance_of String
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "raises an error if the instance doesn't implement get_links" do
|
48
|
+
->{ DownloadTV::LinkGrabber.new("").get_links("test") }.must_raise NotImplementedError
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
describe DownloadTV::Torrent do
|
4
|
+
before do
|
5
|
+
@t = DownloadTV::Torrent.new
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "when creating the object" do
|
9
|
+
it "will have some grabbers" do
|
10
|
+
@t.g_names.empty?.must_equal false
|
11
|
+
@t.g_instances.empty?.must_equal false
|
12
|
+
@t.n_grabbers.must_be :>, 0
|
13
|
+
end
|
14
|
+
|
15
|
+
it "will have the right amount of grabbers" do
|
16
|
+
# Initiakize calls change_grabbers
|
17
|
+
@t.n_grabbers.must_equal @t.g_names.size + 1
|
18
|
+
@t.g_instances.size.must_equal 1
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
it "will populate the instances" do
|
23
|
+
@t.n_grabbers.times.each { @t.change_grabbers }
|
24
|
+
@t.g_names.empty?.must_equal true
|
25
|
+
@t.g_instances.empty?.must_equal false
|
26
|
+
@t.g_instances.size.must_equal @t.n_grabbers
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: Test filter_shows(links)
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: download_tv
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- guille
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: json
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mechanize
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: date
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: io-console
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- guillerg96@gmail.com
|
114
|
+
executables:
|
115
|
+
- tv
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- ".gitignore"
|
120
|
+
- ".travis.yml"
|
121
|
+
- Gemfile
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- bin/tv
|
125
|
+
- download_tv.gemspec
|
126
|
+
- lib/download_tv.rb
|
127
|
+
- lib/download_tv/config_example.rb
|
128
|
+
- lib/download_tv/downloader.rb
|
129
|
+
- lib/download_tv/grabbers/addic7ed.rb
|
130
|
+
- lib/download_tv/grabbers/eztv.rb
|
131
|
+
- lib/download_tv/grabbers/torrentapi.rb
|
132
|
+
- lib/download_tv/grabbers/tpb.rb
|
133
|
+
- lib/download_tv/linkgrabber.rb
|
134
|
+
- lib/download_tv/myepisodes.rb
|
135
|
+
- lib/download_tv/subtitles.rb
|
136
|
+
- lib/download_tv/torrent.rb
|
137
|
+
- lib/download_tv/version.rb
|
138
|
+
- test/downloader_test.rb
|
139
|
+
- test/grabbers_test.rb
|
140
|
+
- test/test_helper.rb
|
141
|
+
- test/torrent_test.rb
|
142
|
+
homepage: https://github.com/guille/download_tv
|
143
|
+
licenses: []
|
144
|
+
metadata: {}
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
requirements: []
|
160
|
+
rubyforge_project:
|
161
|
+
rubygems_version: 2.6.12
|
162
|
+
signing_key:
|
163
|
+
specification_version: 4
|
164
|
+
summary: DownloadTV finds the next episodes from shows you follow on MyEpisodes and
|
165
|
+
downloads them.
|
166
|
+
test_files:
|
167
|
+
- test/downloader_test.rb
|
168
|
+
- test/grabbers_test.rb
|
169
|
+
- test/test_helper.rb
|
170
|
+
- test/torrent_test.rb
|