suby 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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