download_tv 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/guille/daily-shows.svg?branch=master)](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
|