suby 0.0.8 → 0.1.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 CHANGED
@@ -1 +1,3 @@
1
1
  *.gem
2
+ spec/fixtures/*
3
+ !spec/fixtures/.gitkeep
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Find and download subtitles
4
4
 
5
- suby is a little script to find and download subtitles for TV series
5
+ `suby` is a little script to find and download subtitles for TV series
6
6
 
7
7
  ## Install
8
8
 
@@ -10,14 +10,18 @@ suby is a little script to find and download subtitles for TV series
10
10
 
11
11
  ## Synopsis
12
12
 
13
- suby 'my show 1x01 - Pilot.avi' # => Downloads my show 1x01 - Pilot.srt
13
+ suby 'My Show 1x01 - Pilot.avi' # => Downloads 'My Show 1x01 - Pilot.srt'
14
14
 
15
- ## Status
15
+ ## Features
16
16
 
17
- Under heavy development.
17
+ * Parse filename to detect show, season and episode
18
+ * Search and download appropriate subtitle, extracting it from the archive and renaming it
19
+ * Accept a lang option (defaults to en)
20
+ * Try multiple sites, falling back on the next one if it was not found on the current
21
+ * Detailed error messages
18
22
 
19
- * Currently only accept one format for input.
20
- You could use [tvnamer](https://github.com/dbr/tvnamer) to rename the video files before.
23
+ ## TODO
21
24
 
22
- * Search only on tvsubtitles.net.
23
- Many series have subtitles there, but not all.
25
+ * usual movies support (via opensubtitles.org)
26
+ * multi-episodes support
27
+ * choose wiser the right subtitle if many are available
@@ -1,5 +1,7 @@
1
1
  module Suby
2
2
  class Downloader::Addic7ed < Downloader
3
+ Downloader.add(self)
4
+
3
5
  SITE = 'www.addic7ed.com'
4
6
  FORMAT = :file
5
7
  LANG_IDS = {
@@ -7,21 +9,45 @@ module Suby
7
9
  es: 5,
8
10
  fr: 8
9
11
  }
10
- FILTER_IGNORED = "Couldn't find any subs with the specified language. Filter ignored"
12
+ FILTER_IGNORED = "Couldn't find any subs with the specified language. " +
13
+ "Filter ignored"
14
+
15
+ def subtitles_url
16
+ "/serie/#{CGI.escape show}/#{season}/#{episode}/#{LANG_IDS[lang]}"
17
+ end
18
+
19
+ def subtitles_response
20
+ response = get(subtitles_url, {}, false)
21
+ unless Net::HTTPSuccess === response
22
+ raise NotFoundError, "show/season/episode not found"
23
+ end
24
+ response
25
+ end
26
+
27
+ def subtitles_body
28
+ body = subtitles_response.body
29
+ if body.include? FILTER_IGNORED
30
+ raise NotFoundError, "no subtitle available"
31
+ end
32
+ body
33
+ end
34
+
35
+ def redirected_url download_url
36
+ header = { 'Referer' => "http://#{SITE}#{subtitles_url}" }
37
+ location = get_redirection download_url, header # They check Referer
38
+ if location == '/downloadexceeded.php'
39
+ raise NotFoundError, "download exceeded"
40
+ end
41
+ URI.escape location
42
+ end
11
43
 
12
44
  def download_url
13
- subtitles_url = "/serie/#{CGI.escape show}/#{season}/#{episode}/#{LANG_IDS[lang]}"
14
- response = http.get(subtitles_url)
15
- throw :downloader, "show/season/episode not found" unless Net::HTTPSuccess === response
16
- body = response.body
17
- throw :downloader, "no subtitle available" if body.include? FILTER_IGNORED
18
- download_url = Nokogiri(body).css('a').find { |a|
45
+ download_url = Nokogiri(subtitles_body).css('a').find { |a|
19
46
  a[:href].start_with? '/original/' or
20
47
  a[:href].start_with? '/updated/'
21
48
  }[:href]
22
- location = get_redirection download_url, 'Referer' => "http://#{SITE}#{subtitles_url}" # They check Referer
23
- throw :downloader, "download exceeded" if location == '/downloadexceeded.php'
24
- URI.escape location
49
+
50
+ redirected_url download_url
25
51
  end
26
52
  end
27
53
  end
@@ -1,5 +1,7 @@
1
1
  module Suby
2
2
  class Downloader::TVSubtitles < Downloader
3
+ Downloader.add(self)
4
+
3
5
  SITE = 'www.tvsubtitles.net'
4
6
  FORMAT = :zip
5
7
  SEARCH_URL = '/search.php'
@@ -8,20 +10,22 @@ module Suby
8
10
  SHOW_URLS = {}
9
11
  SHOW_PAGES = {}
10
12
 
13
+ # "Show (2009-2011)" => "Show"
14
+ def clean_show_name show_name
15
+ show_name.sub(/ \(\d{4}-\d{4}\)$/, '')
16
+ end
17
+
11
18
  def show_url
12
19
  SHOW_URLS[show] ||= begin
13
- post = Net::HTTP::Post.new(SEARCH_URL)
14
- post.form_data = { 'q' => show }
15
- results = Nokogiri http.request(post).body
20
+ results = Nokogiri(post(SEARCH_URL, 'q' => show))
16
21
  a = results.css('ul li div a').find { |a|
17
- # "Show (2009-2011)" => "Show"
18
- a.text.sub(/ \(\d{4}-\d{4}\)$/, '').casecmp(show) == 0
22
+ clean_show_name(a.text).casecmp(show) == 0
19
23
  }
20
- throw :downloader, "show not found" unless a
24
+ raise NotFoundError, "show not found" unless a
21
25
  url = a[:href]
22
26
 
23
- raise 'could not find the show' unless /^\/tvshow-(\d+)\.html$/ =~ url
24
- "/tvshow-#{$1}.html"
27
+ raise 'invalid show url' unless /^\/tvshow-\d+\.html$/ =~ url
28
+ url
25
29
  end
26
30
  end
27
31
 
@@ -29,37 +33,51 @@ module Suby
29
33
  show_url.sub(/\.html$/, "-#{season}.html")
30
34
  end
31
35
 
36
+ def has_season?
37
+ season_text = "Season #{season}"
38
+ bs = SHOW_PAGES[show].css('div.left_articles p.description b')
39
+ unless bs.find { |b| b.text == season_text }
40
+ raise NotFoundError, "season not found"
41
+ end
42
+ end
43
+
44
+ def find_episode_row
45
+ row = SHOW_PAGES[show].css('div.left_articles table tr').find { |tr|
46
+ tr.children.find { |td| td.name == 'td' and
47
+ td.text =~ /\A#{season}x0?#{episode}\z/ }
48
+ }
49
+ raise NotFoundError, "episode not found" unless row
50
+ row
51
+ end
52
+
32
53
  def episode_url
33
54
  @episode_url ||= begin
34
- SHOW_PAGES[show] ||= Nokogiri get season_url
35
-
36
- season_text = /^Season #{season}$/
37
- SHOW_PAGES[show].css('div.left_articles p.description b').find { |b|
38
- b.text =~ season_text
39
- } or throw :downloader, "season not found"
55
+ SHOW_PAGES[show] ||= Nokogiri(get(season_url))
56
+ has_season?
40
57
 
41
58
  url = nil
42
- SHOW_PAGES[show].css('div.left_articles table tr').find { |tr|
43
- tr.children.find { |td| td.name == 'td' && td.text =~ /\A#{season}x0?#{episode}\z/ }
44
- }.tap { |tr|
45
- throw :downloader, "episode not found" unless tr
46
- }.children.find { |td|
59
+ find_episode_row.children.find { |td|
47
60
  td.children.find { |a|
48
- a.name == 'a' && a[:href].start_with?('episode') && url = a[:href]
61
+ a.name == 'a' and a[:href].start_with?('episode') and url = a[:href]
49
62
  }
50
63
  }
51
- raise "invalid episode url: #{episode_url}" unless url =~ /^episode-(\d+)\.html$/
64
+ unless url =~ /^episode-(\d+)\.html$/
65
+ raise "invalid episode url: #{episode_url}"
66
+ end
67
+
52
68
  "/episode-#{$1}-#{lang}.html"
53
69
  end
54
70
  end
55
71
 
56
72
  def subtitles_url
57
73
  @subtitles_url ||= begin
58
- subtitles = Nokogiri get episode_url
74
+ subtitles = Nokogiri(get(episode_url))
59
75
 
60
76
  # TODO: choose 720p or most downloaded instead of first found
61
- a = subtitles.css('div.left_articles a').find { |a| a.name == 'a' && a[:href].start_with?('/subtitle') }
62
- throw :downloader, "no subtitle available" unless a
77
+ a = subtitles.css('div.left_articles a').find { |a|
78
+ a.name == 'a' and a[:href].start_with?('/subtitle')
79
+ }
80
+ raise NotFoundError, "no subtitle available" unless a
63
81
  url = a[:href]
64
82
  raise 'invalid subtitle url' unless url =~ /^\/subtitle-(\d+)\.html/
65
83
  url
@@ -67,7 +85,8 @@ module Suby
67
85
  end
68
86
 
69
87
  def download_url
70
- @download_url ||= URI.escape '/' + get_redirection(subtitles_url.sub('subtitle', 'download'))
88
+ @download_url ||= URI.escape '/' +
89
+ get_redirection(subtitles_url.sub('subtitle', 'download'))
71
90
  end
72
91
  end
73
92
  end
@@ -6,8 +6,8 @@ require_relative 'filename_parser'
6
6
  module Suby
7
7
  class Downloader
8
8
  DOWNLOADERS = []
9
- def self.inherited(subclass)
10
- DOWNLOADERS << subclass
9
+ def self.add(downloader)
10
+ DOWNLOADERS << downloader
11
11
  end
12
12
 
13
13
  attr_reader :show, :season, :episode, :file, :lang
@@ -21,17 +21,36 @@ module Suby
21
21
  @http ||= Net::HTTP.new(self.class::SITE).start
22
22
  end
23
23
 
24
- def get(path, initheader = {})
24
+ def get(path, initheader = {}, parse_response = true)
25
25
  response = http.get(path, initheader)
26
- raise "Invalid response for #{path}: #{response}" unless Net::HTTPSuccess === response
26
+ if parse_response
27
+ unless Net::HTTPSuccess === response
28
+ raise DownloaderError, "Invalid response for #{path}: #{response}"
29
+ end
30
+ response.body
31
+ else
32
+ response
33
+ end
34
+ end
35
+
36
+ def post(path, data = {}, initheader = {})
37
+ post = Net::HTTP::Post.new(path, initheader)
38
+ post.form_data = data
39
+ response = http.request(post)
40
+ unless Net::HTTPSuccess === response
41
+ raise DownloaderError, "Invalid response for #{path}(#{data}): " +
42
+ response.inspect
43
+ end
27
44
  response.body
28
45
  end
29
46
 
30
47
  def get_redirection(path, initheader = {})
31
48
  response = http.get(path, initheader)
32
49
  location = response['Location']
33
- unless (Net::HTTPFound === response or Net::HTTPSuccess === response) and location
34
- raise "Invalid response for #{path}: #{response}: location: #{location.inspect}"
50
+ unless (Net::HTTPFound === response or
51
+ Net::HTTPSuccess === response) and location
52
+ raise DownloaderError, "Invalid response for #{path}: " +
53
+ "#{response}: location: #{location.inspect}, #{response.body}"
35
54
  end
36
55
  location
37
56
  end
@@ -44,17 +63,23 @@ module Suby
44
63
  contents = get(url)
45
64
  http.finish
46
65
  format = self.class::FORMAT
47
- if format == :file
66
+ case format
67
+ when :file
48
68
  open(sub_name(url), 'wb') { |f| f.write contents }
49
- else
69
+ when :zip
50
70
  open(TEMP_ARCHIVE_NAME, 'wb') { |f| f.write contents }
51
- sub = Suby.extract_sub_from_archive(TEMP_ARCHIVE_NAME, format)
52
- File.rename sub, sub_name(sub)
71
+ Suby.extract_sub_from_archive(TEMP_ARCHIVE_NAME, format, basename)
72
+ else
73
+ raise "unknown subtitles format: #{format}"
53
74
  end
54
75
  end
55
76
 
77
+ def basename
78
+ File.basename(file, File.extname(file))
79
+ end
80
+
56
81
  def sub_name(sub)
57
- File.basename(file, File.extname(file)) + File.extname(sub)
82
+ basename + File.extname(sub)
58
83
  end
59
84
  end
60
85
  end
@@ -0,0 +1,4 @@
1
+ module Suby
2
+ class DownloaderError < StandardError
3
+ end
4
+ end
@@ -79,9 +79,10 @@ module Suby
79
79
 
80
80
  def parse(file)
81
81
  filename = File.basename(file)
82
- FILENAME_PATTERNS.find { |pattern|
82
+ found = FILENAME_PATTERNS.find { |pattern|
83
83
  pattern =~ filename
84
- } or raise "wrong file format (#{file})"
84
+ }
85
+ raise "wrong file format (#{file})" unless found
85
86
  [clean_show_name($~[:show]), $~[:season].to_i, $~[:episode].to_i]
86
87
  end
87
88
 
@@ -0,0 +1,4 @@
1
+ module Suby
2
+ class NotFoundError < StandardError
3
+ end
4
+ end
data/lib/suby.rb CHANGED
@@ -1,4 +1,7 @@
1
+ require_relative 'suby/downloader_error'
2
+ require_relative 'suby/not_found_error'
1
3
  require_relative 'suby/downloader'
4
+ require 'zip/zip'
2
5
 
3
6
  module Suby
4
7
  extend self
@@ -8,46 +11,61 @@ module Suby
8
11
 
9
12
  def download_subtitles(files, options = {})
10
13
  files.each { |file|
11
- next if SUB_EXTENSIONS.include? File.extname(file)
12
14
  next puts "Skipping: #{file}" if SUB_EXTENSIONS.any? { |ext|
13
- File.exist? File.basename(file, File.extname(file)) + ".#{ext}" }
15
+ File.exist? File.basename(file, File.extname(file)) + ".#{ext}"
16
+ }
17
+ download_subtitles_for_file(file, options)
18
+ }
19
+ end
14
20
 
15
- begin
16
- success = Downloader::DOWNLOADERS.find do |downloader|
17
- error = catch :downloader do
18
- downloader.new(file, options[:lang]).download
19
- :success
20
- end
21
- if error == :success
22
- puts "#{downloader} found subtitles for #{file}"
23
- else
24
- puts "#{downloader} did not find subtitles for #{file} (#{error})"
25
- end
26
- error == :success
27
- end
28
- STDERR.puts "No downloader could find subtitles for #{file}" unless success
29
- rescue
30
- puts " The download of the subtitles failed for #{file}:"
31
- puts " #{$!.class}: #{$!.message}"
32
- puts $!.backtrace.map { |line| line.prepend ' '*4 }
21
+ def download_subtitles_for_file(file, options)
22
+ begin
23
+ success = Downloader::DOWNLOADERS.find { |downloader_class|
24
+ try_downloader(downloader_class.new(file, options[:lang]))
25
+ }
26
+ unless success
27
+ STDERR.puts "No downloader could find subtitles for #{file}"
33
28
  end
34
- }
29
+ rescue
30
+ puts " The download of the subtitles failed for #{file}:"
31
+ puts " #{$!.class}: #{$!.message}"
32
+ puts $!.backtrace.map { |line| line.prepend ' '*4 }
33
+ end
35
34
  end
36
35
 
37
- def extract_sub_from_archive(archive, format)
36
+ def try_downloader(downloader)
37
+ begin
38
+ downloader.download
39
+ rescue Suby::NotFoundError => error
40
+ puts "#{downloader.class} did not find subtitles for " +
41
+ "#{downloader.file} (#{error.message})"
42
+ false
43
+ rescue Suby::DownloaderError => error
44
+ puts "#{downloader.class} had a problem finding subtitles for " +
45
+ "#{downloader.file} (#{error.message})"
46
+ false
47
+ else
48
+ puts "#{downloader.class} found subtitles for #{downloader.file}"
49
+ true
50
+ end
51
+ end
52
+
53
+ def extract_sub_from_archive(archive, format, basename)
38
54
  case format
39
55
  when :zip
40
- sub = `unzip -qql #{archive}`.scan(/\d{2}:\d{2} (.+?(?:#{SUB_EXTENSIONS.join '|'}))$/).map(&:first).first
41
- raise "no subtitles in #{archive}" unless sub
42
- sub_for_unzip = sub.gsub(/(\[|\])/) { "\\#{$1}" }
43
- system 'unzip', archive, sub_for_unzip, 1 => :close
44
- puts "found subtitle: #{sub}" if $VERBOSE
56
+ Zip::ZipFile.open(archive) { |zip|
57
+ sub = zip.entries.find { |entry|
58
+ entry.to_s =~ /\.#{Regexp.union SUB_EXTENSIONS}$/
59
+ }
60
+ raise "no subtitles in #{archive}" unless sub
61
+ name = basename + File.extname(sub.to_s)
62
+ sub.extract(name)
63
+ }
45
64
  else
46
65
  raise "unknown archive type (#{archive})"
47
66
  end
48
-
67
+ ensure
49
68
  # Cleaning
50
- File.unlink archive
51
- sub
69
+ File.unlink archive if File.exist? archive
52
70
  end
53
71
  end
data/suby.gemspec CHANGED
@@ -3,12 +3,15 @@ Gem::Specification.new do |s|
3
3
  s.summary = "Subtitles' downloader"
4
4
  s.description = "Find and download subtitles"
5
5
  s.author = 'eregon'
6
+ s.email = 'eregontp@gmail.com'
7
+ s.homepage = 'https://github.com/eregon/suby'
6
8
 
7
9
  s.files = Dir['bin/*', 'lib/**/*.rb', '.gitignore', 'README.md', 'suby.gemspec']
8
10
  s.executables = ['suby']
9
11
 
10
12
  s.required_ruby_version = '>= 1.9.2'
11
13
  s.add_dependency 'nokogiri'
14
+ s.add_dependency 'rubyzip'
12
15
 
13
- s.version = '0.0.8'
16
+ s.version = '0.1.0'
14
17
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-07-27 00:00:00.000000000 Z
12
+ date: 2011-07-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
16
- requirement: &2157439960 !ruby/object:Gem::Requirement
16
+ requirement: &2153546620 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,9 +21,20 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2157439960
24
+ version_requirements: *2153546620
25
+ - !ruby/object:Gem::Dependency
26
+ name: rubyzip
27
+ requirement: &2153546120 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2153546120
25
36
  description: Find and download subtitles
26
- email:
37
+ email: eregontp@gmail.com
27
38
  executables:
28
39
  - suby
29
40
  extensions: []
@@ -33,12 +44,14 @@ files:
33
44
  - lib/suby/downloader/addic7ed.rb
34
45
  - lib/suby/downloader/tvsubtitles.rb
35
46
  - lib/suby/downloader.rb
47
+ - lib/suby/downloader_error.rb
36
48
  - lib/suby/filename_parser.rb
49
+ - lib/suby/not_found_error.rb
37
50
  - lib/suby.rb
38
51
  - .gitignore
39
52
  - README.md
40
53
  - suby.gemspec
41
- homepage:
54
+ homepage: https://github.com/eregon/suby
42
55
  licenses: []
43
56
  post_install_message:
44
57
  rdoc_options: []
@@ -58,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
71
  version: '0'
59
72
  requirements: []
60
73
  rubyforge_project:
61
- rubygems_version: 1.8.5.1
74
+ rubygems_version: 1.8.6
62
75
  signing_key:
63
76
  specification_version: 3
64
77
  summary: Subtitles' downloader