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 +2 -0
- data/README.md +12 -8
- data/lib/suby/downloader/addic7ed.rb +36 -10
- data/lib/suby/downloader/tvsubtitles.rb +44 -25
- data/lib/suby/downloader.rb +36 -11
- data/lib/suby/downloader_error.rb +4 -0
- data/lib/suby/filename_parser.rb +3 -2
- data/lib/suby/not_found_error.rb +4 -0
- data/lib/suby.rb +48 -30
- data/suby.gemspec +4 -1
- metadata +20 -7
data/.gitignore
CHANGED
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 '
|
13
|
+
suby 'My Show 1x01 - Pilot.avi' # => Downloads 'My Show 1x01 - Pilot.srt'
|
14
14
|
|
15
|
-
##
|
15
|
+
## Features
|
16
16
|
|
17
|
-
|
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
|
-
|
20
|
-
You could use [tvnamer](https://github.com/dbr/tvnamer) to rename the video files before.
|
23
|
+
## TODO
|
21
24
|
|
22
|
-
*
|
23
|
-
|
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.
|
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
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
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
|
-
|
18
|
-
a.text.sub(/ \(\d{4}-\d{4}\)$/, '').casecmp(show) == 0
|
22
|
+
clean_show_name(a.text).casecmp(show) == 0
|
19
23
|
}
|
20
|
-
|
24
|
+
raise NotFoundError, "show not found" unless a
|
21
25
|
url = a[:href]
|
22
26
|
|
23
|
-
raise '
|
24
|
-
|
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
|
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
|
-
|
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'
|
61
|
+
a.name == 'a' and a[:href].start_with?('episode') and url = a[:href]
|
49
62
|
}
|
50
63
|
}
|
51
|
-
|
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
|
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|
|
62
|
-
|
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 '/' +
|
88
|
+
@download_url ||= URI.escape '/' +
|
89
|
+
get_redirection(subtitles_url.sub('subtitle', 'download'))
|
71
90
|
end
|
72
91
|
end
|
73
92
|
end
|
data/lib/suby/downloader.rb
CHANGED
@@ -6,8 +6,8 @@ require_relative 'filename_parser'
|
|
6
6
|
module Suby
|
7
7
|
class Downloader
|
8
8
|
DOWNLOADERS = []
|
9
|
-
def self.
|
10
|
-
DOWNLOADERS <<
|
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
|
-
|
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
|
34
|
-
|
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
|
-
|
66
|
+
case format
|
67
|
+
when :file
|
48
68
|
open(sub_name(url), 'wb') { |f| f.write contents }
|
49
|
-
|
69
|
+
when :zip
|
50
70
|
open(TEMP_ARCHIVE_NAME, 'wb') { |f| f.write contents }
|
51
|
-
|
52
|
-
|
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
|
-
|
82
|
+
basename + File.extname(sub)
|
58
83
|
end
|
59
84
|
end
|
60
85
|
end
|
data/lib/suby/filename_parser.rb
CHANGED
@@ -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
|
-
}
|
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
|
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
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
|
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-
|
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: &
|
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: *
|
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.
|
74
|
+
rubygems_version: 1.8.6
|
62
75
|
signing_key:
|
63
76
|
specification_version: 3
|
64
77
|
summary: Subtitles' downloader
|