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 +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
|