download_tv 2.6.5 → 2.6.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3bb73c3cccddd294060908acd7d79d619b1af0927aa51b5c7b7ac04aff6dd20
4
- data.tar.gz: ea159d175a5bcdc1bdf71c35264756ee41d7da2d9b53aa7169258c37926279cf
3
+ metadata.gz: 2cd494e098abb85a02a4a7182e66aef862aa14a04e73bd0c38f2e3a8991c1954
4
+ data.tar.gz: 275eccd2336a88bda38cd0711d169b7e95b9fd492164ae033b199b8d08eb4f32
5
5
  SHA512:
6
- metadata.gz: f052f503bea9ceec1c5010b174680cd9efeb5f657d0db0747018cc232c2a66468e394a574776231472cb3506adf0f3a01def502e0adaa9dd86aa39a42e3328d4
7
- data.tar.gz: 3a5d5d52294d770adc7c73879568c3ccfb88d7e88110bb78d02e7671b883cf8be88ad4e54e2c951d15f8508529d464b5014b4525068982c88598d80ae037830c
6
+ metadata.gz: 884a973095e183b1499839d3b68f01e0ba475ae6ea1220f7a5a98d9f3427892bb78c5404aedc2cbdab6f8f157b9a84729ebd75a1aaef37b5a389670f98e67c40
7
+ data.tar.gz: 2a1d58fb22524127f7fe42040a652473f0146aaa7ea6a7c45fc6af35800d4c09e45ed821ec6ebd8c343e76952af3ec4a4771dfb5986c9f64bbf764347c0bc267
@@ -32,4 +32,4 @@ jobs:
32
32
  ruby-version: ${{ matrix.ruby-version }}
33
33
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
34
  - name: Run tests
35
- run: bundle exec rake
35
+ run: bundle exec rspec
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # download_tv CHANGELOG
2
2
 
3
+ ## 2.6.7 (2022-10-22)
4
+
5
+ * Features
6
+ * Add `--healthcheck` to check the status of all the grabbers (online/offline).
7
+
8
+ * Fixes
9
+ * Better detection for offline grabbers without crashing the app
10
+
11
+ * Grabbers
12
+ * Torrentz: Fix parser
13
+
14
+ ## 2.6.6 (2022-01-21)
15
+
16
+ * Improvements
17
+ * The `--dry-run` option now prevents from persisting any configuration, including pending shows, not just the last execution date.
18
+ * Performance improvements when running with the `-t` flag.
19
+
3
20
  ## 2.6.5 (2021-06-10)
4
21
 
5
22
  * Fixes
data/Gemfile CHANGED
@@ -3,3 +3,5 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
+
7
+ gem "rspec", "~> 3.11"
data/README.md CHANGED
@@ -6,13 +6,13 @@
6
6
 
7
7
  **download_tv** is a tool that allows the user to find magnet links for TV show episodes. It accepts shows as arguments, from a file or it can integrate with your MyEpisodes account.
8
8
 
9
- ### Installation
9
+ ## Installation
10
10
 
11
11
  `gem install download_tv`
12
12
 
13
- ### Usage
13
+ ## Usage
14
14
 
15
- Once installed, you can launch the binary *tv*
15
+ Once installed, you can launch the binary `tv`
16
16
 
17
17
  ```
18
18
  Usage: tv [options]
@@ -29,6 +29,7 @@ Specific options:
29
29
  -a, --[no-]auto Automatically find links
30
30
  -g, --grabber GRABBER Use given grabber as first option
31
31
  --show-grabbers List available grabbers
32
+ --healthcheck Check status of all the grabbers
32
33
  -p, --pending Show list of pending downloads
33
34
  --clear-pending Clear list of pending downloads
34
35
  -q, --queue SHOW Add show episode to pending downloads list
@@ -86,6 +87,10 @@ Upon installation, the default filters exclude 2060p, 1080p or 720p, and include
86
87
 
87
88
  Keep in mind that this is not a hard filter. The application will sequentially apply as many user-defined filters as possible **while still returning at least one result**.
88
89
 
89
- ### License
90
+ ## Shell completion
91
+
92
+ The provided binary will print completion files to STDOUT by passing the options `--*-completion-bash` and `--*-completion-zsh` (you might have to escape the asterisk). Use your shell's manual to find out how to load these files to get completions.
93
+
94
+ ## License
90
95
 
91
96
  This project is released under the terms of the MIT license. See [LICENSE.md](https://github.com/guille/download_tv/blob/master/LICENSE.md) file for details.
data/bin/tv CHANGED
@@ -47,7 +47,7 @@ opt_parser = OptionParser.new do |opts|
47
47
  options[:cmd] = 'showconfig'
48
48
  end
49
49
 
50
- opts.on('--dry-run', "Don't write to the date file") do |n|
50
+ opts.on('--dry-run', "Don't update the persisted configuration") do |n|
51
51
  options[:dry] = n
52
52
  end
53
53
 
@@ -60,7 +60,12 @@ opt_parser = OptionParser.new do |opts|
60
60
  end
61
61
 
62
62
  opts.on('--show-grabbers', 'List available grabbers') do
63
- puts DownloadTV::Torrent.new.grabbers
63
+ puts DownloadTV::Torrent.grabbers
64
+ exit
65
+ end
66
+
67
+ opts.on('--healthcheck', 'Check status of all the grabbers') do
68
+ DownloadTV::Torrent.healthcheck
64
69
  exit
65
70
  end
66
71
 
@@ -117,9 +122,9 @@ begin
117
122
  when 'config'
118
123
  DownloadTV::Configuration.new(config).change_configuration
119
124
  when 'showconfig'
120
- DownloadTV::Configuration.new(config).print_config
125
+ puts DownloadTV::Configuration.new(config)
121
126
  when 'showpending'
122
- DownloadTV::Configuration.new(config).print_attr(:pending)
127
+ puts DownloadTV::Configuration.new(config)[:pending]
123
128
  when 'clearpending'
124
129
  DownloadTV::Configuration.new(config).clear_pending
125
130
  when 'queue'
@@ -4,134 +4,160 @@ module DownloadTV
4
4
  ##
5
5
  # Class used for managing the configuration of the application
6
6
  class Configuration
7
- attr_reader :content, :config_path
7
+ def initialize(user_config = {})
8
+ load_config(user_config)
9
+ end
10
+
11
+ def [](key)
12
+ @content[key]
13
+ end
14
+
15
+ def []=(key, value)
16
+ @content[key] = value
17
+ end
18
+
19
+ def change_configuration
20
+ prompt_for_new_values
21
+ set_default_values
22
+ serialize
23
+ end
24
+
25
+ def serialize
26
+ self[:pending] = self[:pending].uniq
27
+ File.write(config_path, JSON.generate(@content))
28
+ end
29
+
30
+ def to_s
31
+ @content.reduce('') do |mem, item|
32
+ key, val = item
33
+ "#{mem}#{key}: #{val}\n"
34
+ end
35
+ end
36
+
37
+ def clear_pending
38
+ self[:pending].clear
39
+ serialize
40
+ end
41
+
42
+ def queue_pending(show)
43
+ self[:pending] << show
44
+ serialize
45
+ end
46
+
47
+ private
8
48
 
9
- def initialize(content = {})
10
- @config_path = content[:path] || default_config_path
11
- FileUtils.mkdir_p(File.expand_path('..', @config_path))
49
+ def content
50
+ @content ||= {}
51
+ end
12
52
 
13
- if File.exist? @config_path
14
- load_config
15
- @content.merge!(content) unless content.empty?
16
- @content[:ignored]&.map!(&:downcase)
53
+ def load_config(user_config)
54
+ if File.exist? config_path
55
+ parse_config
17
56
  else
18
- @content = content
57
+ FileUtils.mkdir_p(File.expand_path('..', config_path))
19
58
  change_configuration
20
59
  end
60
+ content.merge!(user_config) unless user_config.empty?
61
+ self[:ignored]&.map!(&:downcase)
21
62
  end
22
63
 
23
- def change_configuration
64
+ def parse_config
65
+ source = File.read(config_path)
66
+ @content = JSON.parse(source, symbolize_names: true)
67
+
68
+ self[:date] = Date.parse(self[:date]) if self[:date]
69
+
70
+ if !self[:version] || breaking_changes?(self[:version])
71
+ warn 'Change configuration required (version with breaking changes detected)'
72
+ change_configuration
73
+ end
74
+ rescue JSON::ParserError => e
75
+ warn "Error parsing config file at #{config_path} => #{e.message}"
76
+ change_configuration
77
+ end
78
+
79
+ ##
80
+ # Returns true if a major or minor update has been detected, or if the config version is newer
81
+ # than the installed version. Returns something falsy otherwise
82
+ def breaking_changes?(version)
83
+ paired = DownloadTV::VERSION.split('.')
84
+ .map(&:to_i)
85
+ .zip(version.split('.').map(&:to_i))
86
+ # The configuration belongs to a newer version than is installed
87
+ return true unless paired.find_index { |x, y| x < y }.nil?
88
+
89
+ paired.find_index { |x, y| y < x }&.< 2
90
+ end
91
+
92
+ def prompt_for_new_values
24
93
  prompt_for_myep_user
25
94
  prompt_for_cookie
26
95
  prompt_for_ignored
27
96
  prompt_for_filters
28
97
  $stdout.flush
29
-
30
- set_default_values
31
- serialize
32
98
  end
33
99
 
34
100
  def prompt_for_myep_user
35
- existing = "(#{@content[:myepisodes_user]}) " if @content[:myepisodes_user]
101
+ existing = "(#{self[:myepisodes_user]}) " if self[:myepisodes_user]
36
102
  print "Enter your MyEpisodes username #{existing}: "
37
103
  input = $stdin.gets.chomp
38
- @content[:myepisodes_user] = input if input
104
+ self[:myepisodes_user] = input if input
39
105
  end
40
106
 
41
107
  def prompt_for_cookie
42
108
  print 'Save cookie? (y)/n: '
43
- @content[:cookie] = !($stdin.gets.chomp.casecmp? 'n')
109
+ self[:cookie] = !($stdin.gets.chomp.casecmp? 'n')
44
110
  end
45
111
 
46
112
  def prompt_for_ignored
47
- existing = "(#{@content[:ignored]})" if @content[:ignored]
113
+ existing = "(#{self[:ignored]})" if self[:ignored]
48
114
  puts "Enter a comma-separated list of shows to ignore: #{existing}"
49
115
 
50
- @content[:ignored] = read_and_split_list :downcase
116
+ self[:ignored] = read_and_split_list :downcase
51
117
  end
52
118
 
53
119
  def prompt_for_filters
54
- puts "Current filters: (#{@content[:filters]})" if @content[:filters]
55
- @content[:filters] = {}
120
+ puts "Current filters: (#{self[:filters]})" if self[:filters]
121
+ self[:filters] = {}
56
122
 
57
123
  puts 'Enter a comma-separated list of terms to include: '
58
- @content[:filters][:includes] = read_and_split_list :upcase
124
+ self[:filters][:includes] = read_and_split_list :upcase
59
125
 
60
126
  puts 'Enter a comma-separated list of terms to exclude: '
61
- @content[:filters][:excludes] = read_and_split_list :upcase
127
+ self[:filters][:excludes] = read_and_split_list :upcase
62
128
  end
63
129
 
64
- def read_and_split_list(case_method)
65
- $stdin.gets.chomp.split(',')
66
- .map(&:strip)
67
- .map(&case_method)
130
+ def config_path
131
+ (content[:path] || default_config_path)
68
132
  end
69
133
 
70
- def default_filters
71
- {
72
- 'includes' => %w[PROPER REPACK],
73
- 'excludes' => %w[2160P 1080P 720P]
74
- }
134
+ def default_config_path
135
+ File.join(ENV['HOME'], '.config', 'download_tv', 'config')
75
136
  end
76
137
 
77
138
  ##
78
139
  # Update the +content+ attribute with the defaults, if needed.
79
140
  # Maintains the previous values, in case it's an update from an existing file.
80
141
  def set_default_values
81
- @content[:auto] ||= true
82
- @content[:grabber] ||= 'TorrentAPI'
83
- @content[:date] ||= Date.today - 1
84
- @content[:filters] ||= default_filters
85
- @content[:pending] ||= []
86
- @content[:version] = DownloadTV::VERSION
87
- end
88
-
89
- def serialize
90
- @content[:pending] = @content[:pending].uniq
91
- File.write(@config_path, JSON.generate(@content))
92
- end
93
-
94
- def load_config
95
- source = File.read(@config_path)
96
- @content = JSON.parse(source, symbolize_names: true)
97
-
98
- @content[:date] = Date.parse(@content[:date]) if @content[:date]
99
-
100
- change_configuration if !@content[:version] || breaking_changes?(@content[:version])
101
- rescue JSON::ParserError
102
- @content = {}
103
- change_configuration
104
- end
105
-
106
- def default_config_path
107
- File.join(ENV['HOME'], '.config', 'download_tv', 'config')
108
- end
109
-
110
- ##
111
- # Returns true if a major or minor update has been detected, something falsy otherwise
112
- def breaking_changes?(version)
113
- DownloadTV::VERSION.split('.')
114
- .zip(version.split('.'))
115
- .find_index { |x, y| y < x }
116
- &.< 2
142
+ self[:auto] ||= true
143
+ self[:grabber] ||= 'TorrentAPI'
144
+ self[:date] ||= Date.today - 1
145
+ self[:filters] ||= default_filters
146
+ self[:pending] ||= []
147
+ self[:version] = DownloadTV::VERSION
117
148
  end
118
149
 
119
- def print_config
120
- @content.each { |k, v| puts "#{k}: #{v}" }
121
- end
122
-
123
- def print_attr(arg)
124
- puts @content[arg]
125
- end
126
-
127
- def clear_pending
128
- @content[:pending].clear
129
- serialize
150
+ def default_filters
151
+ {
152
+ 'includes' => %w[PROPER REPACK],
153
+ 'excludes' => %w[2160P 1080P 720P]
154
+ }
130
155
  end
131
156
 
132
- def queue_pending(show)
133
- @content[:pending] << show
134
- serialize
157
+ def read_and_split_list(case_method)
158
+ $stdin.gets.chomp.split(',')
159
+ .map(&:strip)
160
+ .map(&case_method)
135
161
  end
136
162
  end
137
163
  end
@@ -16,7 +16,7 @@ module DownloadTV
16
16
  # Tries to download episodes in order for a given season,
17
17
  # until it can't find any
18
18
  def download_entire_season(show, season)
19
- t = Torrent.new(@config.content[:grabber])
19
+ t = Torrent.new(@config[:grabber])
20
20
  season.insert(0, '0') if season.size == 1
21
21
  episode = "#{show} s#{season}e01"
22
22
  loop do
@@ -29,7 +29,7 @@ module DownloadTV
29
29
  end
30
30
 
31
31
  def download_single_show(show, season = nil)
32
- t = Torrent.new(@config.content[:grabber])
32
+ t = Torrent.new(@config[:grabber])
33
33
  show = fix_names([show]).first
34
34
  if season
35
35
  download_entire_season(show, season)
@@ -44,7 +44,7 @@ module DownloadTV
44
44
  def download_from_file(filename)
45
45
  if File.exist? filename
46
46
  filename = File.realpath(filename)
47
- t = Torrent.new(@config.content[:grabber])
47
+ t = Torrent.new(@config[:grabber])
48
48
  to_download = File.readlines(filename, chomp: true)
49
49
  fix_names(to_download).each { |show| download(get_link(t, show)) }
50
50
  else
@@ -56,7 +56,7 @@ module DownloadTV
56
56
  ##
57
57
  # Returns the date from which to check shows
58
58
  def date_to_check_from(offset)
59
- return @config.content[:date] if offset.zero?
59
+ return @config[:date] if offset.zero?
60
60
 
61
61
  Date.today - offset
62
62
  end
@@ -66,17 +66,16 @@ module DownloadTV
66
66
  # the last run of the program
67
67
  # It connects to MyEpisodes in order to find which shows
68
68
  # to track and which new episodes aired.
69
- # The param +dont_update_last_run+ prevents changing the configuration's date value
69
+ # The param +dry_run+ prevents changing the persisted configuration
70
70
  # The param +offset+ can be used to move the date back that many days in the check
71
71
  # The param +include_tomorrow+ will add the current day to the list of dates to search
72
- def run(dont_update_last_run, offset = 0, include_tomorrow: false)
73
- pending = @config.content[:pending].clone
74
- @config.content[:pending].clear
72
+ def run(dry_run = false, offset = 0, include_tomorrow: false)
73
+ pending = @config[:pending].clone
74
+ @config[:pending].clear
75
75
  pending ||= []
76
76
  date = date_to_check_from(offset)
77
77
 
78
- pending.concat shows_to_download(date) if date < Date.today
79
- pending.concat today_shows_to_download if include_tomorrow && date < Date.today.next
78
+ pending.concat shows_to_download(date, include_tomorrow) if date < (include_tomorrow ? Date.today.next : Date.today)
80
79
 
81
80
  if pending.empty?
82
81
  puts 'Nothing to download'
@@ -85,14 +84,14 @@ module DownloadTV
85
84
  puts 'Completed. Exiting...'
86
85
  end
87
86
 
88
- unless dont_update_last_run
89
- @config.content[:date] = if include_tomorrow
87
+ unless dry_run
88
+ @config[:date] = if include_tomorrow
90
89
  Date.today.next
91
90
  else
92
- [Date.today, @config.content[:date]].max
91
+ [Date.today, @config[:date]].max
93
92
  end
93
+ @config.serialize
94
94
  end
95
- @config.serialize
96
95
  rescue InvalidLoginError
97
96
  warn 'Wrong username/password combination'
98
97
  end
@@ -126,20 +125,10 @@ module DownloadTV
126
125
  download_t.join
127
126
  end
128
127
 
129
- def shows_to_download(date)
130
- myepisodes = MyEpisodes.new(@config.content[:myepisodes_user],
131
- @config.content[:cookie])
132
- myepisodes.load_cookie
133
- shows = myepisodes.get_shows_since(date)
134
- shows = reject_ignored(shows)
135
- fix_names(shows)
136
- end
137
-
138
- def today_shows_to_download
139
- myepisodes = MyEpisodes.new(@config.content[:myepisodes_user],
140
- @config.content[:cookie])
141
- myepisodes.load_cookie
142
- shows = myepisodes.today_shows
128
+ def shows_to_download(date, include_tomorrow)
129
+ myepisodes = MyEpisodes.new(@config[:myepisodes_user],
130
+ @config[:cookie])
131
+ shows = myepisodes.get_shows_since(date, include_tomorrow: include_tomorrow)
143
132
  shows = reject_ignored(shows)
144
133
  fix_names(shows)
145
134
  end
@@ -154,11 +143,11 @@ module DownloadTV
154
143
  links = torrent.get_links(show)
155
144
 
156
145
  if links.empty?
157
- @config.content[:pending] << show if save_pending
146
+ @config[:pending] << show if save_pending
158
147
  return
159
148
  end
160
149
 
161
- if @config.content[:auto]
150
+ if @config[:auto]
162
151
  filter_shows(links).first[1]
163
152
  else
164
153
  prompt_links(links)
@@ -191,7 +180,7 @@ module DownloadTV
191
180
  def reject_ignored(shows)
192
181
  shows.reject do |i|
193
182
  # Remove season+episode
194
- @config.content[:ignored]
183
+ @config[:ignored]
195
184
  .include?(i.split(' ')[0..-2].join(' ').downcase)
196
185
  end
197
186
  end
@@ -209,7 +198,7 @@ module DownloadTV
209
198
  # Runs until no filters are left to be applied or applying
210
199
  # a filter would leave no results
211
200
  def filter_shows(links)
212
- @filterer ||= Filterer.new(@config.content[:filters])
201
+ @filterer ||= Filterer.new(@config[:filters])
213
202
  @filterer.filter(links)
214
203
  end
215
204
 
@@ -4,28 +4,11 @@ module DownloadTV
4
4
  ##
5
5
  # Builds and applies filters to the results
6
6
  class Filterer
7
- attr_reader :filters
8
-
9
7
  def initialize(filters_config)
10
8
  @filters = []
11
9
  build_filters(filters_config)
12
10
  end
13
11
 
14
- def build_include_filter(str)
15
- @filters << ->(n) { !n.upcase.include?(str) }
16
- end
17
-
18
- def build_exclude_filter(str)
19
- @filters << ->(n) { n.upcase.include?(str) }
20
- end
21
-
22
- def build_filters(filters_config)
23
- return unless filters_config
24
-
25
- filters_config[:includes].map { |i| build_include_filter(i) }
26
- filters_config[:excludes].map { |i| build_exclude_filter(i) }
27
- end
28
-
29
12
  ##
30
13
  # Iteratively applies filters until they've all been applied
31
14
  # or applying the next filter would result in no results
@@ -41,5 +24,22 @@ module DownloadTV
41
24
 
42
25
  shows
43
26
  end
27
+
28
+ private
29
+
30
+ def build_filters(filters_config)
31
+ return unless filters_config
32
+
33
+ filters_config[:includes].map { |i| build_include_filter(i) }
34
+ filters_config[:excludes].map { |i| build_exclude_filter(i) }
35
+ end
36
+
37
+ def build_include_filter(str)
38
+ @filters << ->(n) { !n.upcase.include?(str) }
39
+ end
40
+
41
+ def build_exclude_filter(str)
42
+ @filters << ->(n) { n.upcase.include?(str) }
43
+ end
44
44
  end
45
45
  end
@@ -9,7 +9,7 @@ module DownloadTV
9
9
  end
10
10
 
11
11
  def get_links(show)
12
- raw_data = @agent.get(format(@url, show))
12
+ raw_data = agent.get(format(@url, show))
13
13
  raw_seeders = raw_data.search('td.forum_thread_post_end').map { |e| e.children[0].text.to_i }
14
14
  raw_links = raw_data.search('a.magnet').sort_by.with_index { |_, index| raw_seeders[index] }.reverse
15
15
 
@@ -35,7 +35,7 @@ module DownloadTV
35
35
  # Makes a get request tp the given url.
36
36
  # Returns the JSON response parsed into a hash
37
37
  def request_and_parse(url)
38
- page = @agent.get(url).content
38
+ page = agent.get(url).content
39
39
  JSON.parse(page)
40
40
  end
41
41
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DownloadTV
4
+ ##
5
+ # Torrentz2 grabber
6
+ class Torrentz < LinkGrabber
7
+ def initialize
8
+ super('https://torrentz2.nz/search?q=%s')
9
+ end
10
+
11
+ def get_links(show)
12
+ raw_data = agent.get(format(@url, show))
13
+ results = raw_data.search('dl')
14
+
15
+ raise NoTorrentsError if results.empty?
16
+
17
+ data = results.sort_by { |e| e.search('dd span')[3].text.to_i }.reverse
18
+
19
+ data.collect do |i|
20
+ [i.children[0].text.strip,
21
+ i.search('dd span a').first.attribute('href').text]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -14,7 +14,7 @@ module DownloadTV
14
14
  search = format(@url, show)
15
15
 
16
16
  # Skip the header
17
- data = @agent.get(search).search('#searchResult tr').drop 1
17
+ data = agent.get(search).search('#searchResult tr').drop 1
18
18
 
19
19
  raise NoTorrentsError if data.empty?
20
20
 
@@ -8,7 +8,10 @@ module DownloadTV
8
8
 
9
9
  def initialize(url)
10
10
  @url = url
11
- @agent = Mechanize.new do |a|
11
+ end
12
+
13
+ def agent
14
+ @agent ||= Mechanize.new do |a|
12
15
  a.user_agent = DownloadTV::USER_AGENT
13
16
  a.read_timeout = 10
14
17
  end
@@ -20,9 +23,9 @@ module DownloadTV
20
23
  else
21
24
  @url
22
25
  end
23
- @agent.head(url)
26
+ agent.head(url)
24
27
  true
25
- rescue Mechanize::ResponseCodeError, Net::HTTP::Persistent::Error, Errno::ECONNRESET
28
+ rescue Mechanize::ResponseCodeError, Net::HTTP::Persistent::Error, Errno::ECONNRESET, Net::ReadTimeout, OpenSSL::SSL::SSLError
26
29
  false
27
30
  end
28
31