storyboard 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.gitmodules +3 -0
- data/.rvmrc +1 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +55 -0
- data/README.md +40 -0
- data/TODO +6 -0
- data/bin/storyboard +82 -0
- data/bin/storyboard-ffprobe +0 -0
- data/lib/.DS_Store +0 -0
- data/lib/storyboard/generators/pdf.rb +46 -0
- data/lib/storyboard/generators/sub.rb +32 -0
- data/lib/storyboard/subtitles.rb +96 -0
- data/lib/storyboard/thread-util.rb +308 -0
- data/lib/storyboard/time.rb +34 -0
- data/lib/storyboard/version.rb +3 -0
- data/lib/storyboard.rb +119 -0
- data/storyboard.gemspec +56 -0
- data/vendor/suby/.gitignore +3 -0
- data/vendor/suby/LICENSE +19 -0
- data/vendor/suby/README.md +27 -0
- data/vendor/suby/bin/suby +30 -0
- data/vendor/suby/lib/suby/downloader/addic7ed.rb +65 -0
- data/vendor/suby/lib/suby/downloader/opensubtitles.rb +83 -0
- data/vendor/suby/lib/suby/downloader/tvsubtitles.rb +90 -0
- data/vendor/suby/lib/suby/downloader.rb +177 -0
- data/vendor/suby/lib/suby/filename_parser.rb +103 -0
- data/vendor/suby/lib/suby/interface.rb +17 -0
- data/vendor/suby/lib/suby/movie_hasher.rb +31 -0
- data/vendor/suby/lib/suby.rb +89 -0
- data/vendor/suby/spec/fixtures/.gitkeep +0 -0
- data/vendor/suby/spec/mock_http.rb +22 -0
- data/vendor/suby/spec/spec_helper.rb +3 -0
- data/vendor/suby/spec/suby/downloader/addict7ed_spec.rb +28 -0
- data/vendor/suby/spec/suby/downloader/opensubtitles_spec.rb +33 -0
- data/vendor/suby/spec/suby/downloader/tvsubtitles_spec.rb +50 -0
- data/vendor/suby/spec/suby/downloader_spec.rb +11 -0
- data/vendor/suby/spec/suby/filename_parser_spec.rb +66 -0
- data/vendor/suby/spec/suby_spec.rb +27 -0
- data/vendor/suby/suby.gemspec +20 -0
- metadata +232 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
class STRTime
|
2
|
+
REGEX = /(\d{1,2}):(\d{1,2}):(\d{2})[,\.](\d{1,3})/
|
3
|
+
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
class <<self
|
7
|
+
def parse(str)
|
8
|
+
hh,mm,ss,ms = str.scan(REGEX).flatten.map{|i| Float(i)}
|
9
|
+
value = ((((hh*60)+mm)*60)+ss) + ms/1000
|
10
|
+
self.new(value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(value)
|
15
|
+
@value = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def +(bump)
|
19
|
+
STRTime.new(@value + bump)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_srt
|
23
|
+
ss = @value.floor
|
24
|
+
ms = ((@value - ss)*1000).to_i
|
25
|
+
|
26
|
+
mm = ss / 60
|
27
|
+
ss = ss - mm * 60
|
28
|
+
|
29
|
+
hh = mm / 60
|
30
|
+
mm = mm - hh * 60
|
31
|
+
|
32
|
+
"%02i:%02i:%02i.%03i" % [hh, mm, ss, ms]
|
33
|
+
end
|
34
|
+
end
|
data/lib/storyboard.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'storyboard/subtitles.rb'
|
2
|
+
require 'storyboard/thread-util.rb'
|
3
|
+
require 'storyboard/time.rb'
|
4
|
+
require 'storyboard/version.rb'
|
5
|
+
|
6
|
+
require 'storyboard/generators/sub.rb'
|
7
|
+
require 'storyboard/generators/pdf.rb'
|
8
|
+
|
9
|
+
require 'mime/types'
|
10
|
+
|
11
|
+
class Storyboard
|
12
|
+
attr_accessor :options, :capture_points, :subtitles, :timings
|
13
|
+
attr_accessor :length, :renderers
|
14
|
+
|
15
|
+
def initialize(o)
|
16
|
+
@capture_points = []
|
17
|
+
@renderers = []
|
18
|
+
@options = o
|
19
|
+
|
20
|
+
@options[:save_directory] = File.join(o[:work_dir], 'raw_frames')
|
21
|
+
|
22
|
+
Dir.mkdir(@options[:save_directory]) unless File.directory?(@options[:save_directory])
|
23
|
+
|
24
|
+
@subtitles = SRT.new(options[:subs] ? File.read(options[:subs]) : get_subtitles, options)
|
25
|
+
# temp hack so I don't have to wait all the time.
|
26
|
+
@subtitles.save if options[:verbose]
|
27
|
+
|
28
|
+
@renderers << Storyboard::PDFRenderer.new(self) if options[:types].include?('pdf')
|
29
|
+
|
30
|
+
check_video
|
31
|
+
run_scene_detection if options[:scenes]
|
32
|
+
consolidate_frames
|
33
|
+
extract_frames
|
34
|
+
render_output
|
35
|
+
end
|
36
|
+
|
37
|
+
def run_scene_detection
|
38
|
+
LOG.info("Scanning for scene changes. This may take a moment.")
|
39
|
+
pbar = ProgressBar.create(:title => " Analyzing Video", :format => '%t [%B] %e', :total => @length, :smoothing => 0.6)
|
40
|
+
bin = File.join(File.dirname(__FILE__), '../bin/storyboard-ffprobe')
|
41
|
+
Open3.popen3(bin, "-show_frames", "-of", "compact=p=0", "-f", "lavfi", %(movie=#{options[:file]},select=gt(scene\\,.30)), "-pretty") {|stdin, stdout, stderr, wait_thr|
|
42
|
+
begin
|
43
|
+
# trolololol
|
44
|
+
o = stdout.gets.split('|').inject({}){|hold,value| s = value.split('='); hold[s[0]]=s[1]; hold }
|
45
|
+
t = STRTime.parse(o['pkt_pts_time'])
|
46
|
+
pbar.progress = t.value
|
47
|
+
@capture_points << t
|
48
|
+
end while !stdout.eof?
|
49
|
+
}
|
50
|
+
pbar.finish
|
51
|
+
LOG.info("#{@capture_points.count} scenes registered")
|
52
|
+
end
|
53
|
+
|
54
|
+
def consolidate_frames
|
55
|
+
@subtitles.pages.each {|f| @capture_points << f[:start_time] }
|
56
|
+
@capture_points = @capture_points.sort_by {|cp| cp.value }
|
57
|
+
last_time = STRTime.new(0)
|
58
|
+
removed = 0
|
59
|
+
@capture_points.each_with_index {|ts,i|
|
60
|
+
# while it should be a super rare condition, this should not be
|
61
|
+
# allowed to delete subtitle frames.
|
62
|
+
if (ts.value - last_time.value) < options[:consolidate_frame_threshold]
|
63
|
+
@capture_points.delete_at(i-1) unless i == 0
|
64
|
+
removed += 1
|
65
|
+
end
|
66
|
+
last_time = ts
|
67
|
+
}
|
68
|
+
LOG.debug("Removed #{removed} frames that were within the consolidate_frame_threshold of #{options[:consolidate_frame_threshold]}")
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_frames
|
72
|
+
pool = Thread::Pool.new(8)
|
73
|
+
pbar = ProgressBar.create(:title => " Extracting Frames", :format => '%t [%c/%C|%B] %e', :total => @capture_points.count)
|
74
|
+
|
75
|
+
@capture_points.each_with_index {|f,i|
|
76
|
+
# It's *massively* quicker to jump to a bit before where we want to be, and then make the incrimental jump to
|
77
|
+
# exactly where we want to be.
|
78
|
+
seek_primer = (f.value < 1.000) ? 0 : -1.000
|
79
|
+
# should make Frame a struct with idx and subs
|
80
|
+
image_name = File.join(@options[:save_directory], "%04d.jpg" % [i])
|
81
|
+
pool.process {
|
82
|
+
pbar.increment
|
83
|
+
cmd = ["ffmpeg", "-ss", (f + seek_primer).to_srt, "-i", %("#{options[:file]}"), "-vframes 1", "-ss", STRTime.new(seek_primer.abs).to_srt, %("#{image_name}")].join(' ')
|
84
|
+
Open3.popen3(cmd){|stdin, stdout, stderr, wait_thr|
|
85
|
+
# block the output so it doesn't quit immediately
|
86
|
+
stdout.readlines
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
pool.shutdown
|
91
|
+
LOG.info("Finished Extracting Frames")
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
def render_output
|
96
|
+
LOG.info("Rendering output files")
|
97
|
+
pbar = ProgressBar.create(:title => " Rendering Output", :format => '%t [%c/%C|%B] %e', :total => @capture_points.count)
|
98
|
+
@capture_points.each_with_index {|f,i|
|
99
|
+
image_name = File.join(@options[:save_directory], "%04d.jpg" % [i])
|
100
|
+
capture_point_subtitles = @subtitles.pages.select { |page| f.value >= page.start_time.value and f.value <= page.end_time.value }.first
|
101
|
+
begin
|
102
|
+
@renderers.each{|r| r.render_frame(image_name, capture_point_subtitles) }
|
103
|
+
rescue
|
104
|
+
p $!
|
105
|
+
end
|
106
|
+
pbar.increment
|
107
|
+
}
|
108
|
+
|
109
|
+
@renderers.each {|r| r.write }
|
110
|
+
LOG.info("Finished Rendering Output files")
|
111
|
+
end
|
112
|
+
|
113
|
+
def check_video
|
114
|
+
LOG.debug MIME::Types.type_for(options[:file])
|
115
|
+
@length = `ffmpeg -i "#{options[:file]}" 2>&1 | grep "Duration" | cut -d ' ' -f 4 | sed s/,//`
|
116
|
+
@length = STRTime.parse(length.strip+'0').value
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
data/storyboard.gemspec
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.expand_path('../lib/storyboard/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Mark Olson"]
|
5
|
+
gem.email = ["\"theothermarkolson@gmail.com\""]
|
6
|
+
gem.description = %q{Generate PDFs and eBooks from video files}
|
7
|
+
gem.summary = %q{Video to PDF/ePub generator}
|
8
|
+
gem.homepage = "http://github.com/markolson/storyboard"
|
9
|
+
|
10
|
+
gem.required_ruby_version = '>= 1.9.2'
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($\) if File.exist?('.git')
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.name = "storyboard"
|
16
|
+
gem.require_paths = ["lib","vendor/suby"]
|
17
|
+
gem.version = Storyboard::VERSION
|
18
|
+
|
19
|
+
gem.add_dependency 'nokogiri'
|
20
|
+
gem.add_dependency 'rmagick'
|
21
|
+
gem.add_dependency 'prawn'
|
22
|
+
gem.add_dependency 'ruby-progressbar'
|
23
|
+
|
24
|
+
# suby stuff.
|
25
|
+
gem.add_dependency 'path', '>= 1.3.0'
|
26
|
+
gem.add_dependency 'nokogiri'
|
27
|
+
gem.add_dependency 'rubyzip'
|
28
|
+
gem.add_dependency 'term-ansicolor'
|
29
|
+
gem.add_dependency 'mime-types', '>= 1.19'
|
30
|
+
|
31
|
+
if File.exist?('.git')
|
32
|
+
p "RUNNING"
|
33
|
+
`git submodule --quiet foreach 'echo $path'`.split($\).each do |submodule_path|
|
34
|
+
# for each submodule, change working directory to that submodule
|
35
|
+
Dir.chdir(submodule_path) do
|
36
|
+
# issue git ls-files in submodule's directory
|
37
|
+
submodule_files = `git ls-files`.split($\)
|
38
|
+
|
39
|
+
|
40
|
+
# prepend the submodule path to create absolute file paths
|
41
|
+
submodule_files_fullpaths = submodule_files.map do |filename|
|
42
|
+
"#{submodule_path}/#{filename}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# remove leading path parts to get paths relative to the gem's root dir
|
46
|
+
# (this assumes, that the gemspec resides in the gem's root dir)
|
47
|
+
submodule_files_paths = submodule_files_fullpaths.map do |filename|
|
48
|
+
filename.gsub "#{File.dirname(__FILE__)}/", ""
|
49
|
+
end
|
50
|
+
|
51
|
+
# add relative paths to gem.files
|
52
|
+
gem.files += submodule_files_paths
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/vendor/suby/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2011 Benoit Daloze
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# suby
|
2
|
+
|
3
|
+
Find and download subtitles
|
4
|
+
|
5
|
+
`suby` is a little script to find and download subtitles for TV series
|
6
|
+
|
7
|
+
## Install
|
8
|
+
|
9
|
+
gem install suby
|
10
|
+
|
11
|
+
## Synopsis
|
12
|
+
|
13
|
+
suby 'My Show 1x01 - Pilot.avi' # => Downloads 'My Show 1x01 - Pilot.srt'
|
14
|
+
|
15
|
+
## Features
|
16
|
+
|
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
|
22
|
+
|
23
|
+
## TODO
|
24
|
+
|
25
|
+
* usual movies support (via opensubtitles.org)
|
26
|
+
* multi-episodes support
|
27
|
+
* choose wiser the right subtitle if many are available
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/suby'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
option_parser = OptionParser.new do |opts|
|
8
|
+
opts.banner = "#{File.basename $0} [options] video"
|
9
|
+
opts.separator ' Find and download subtitles for the given video file'
|
10
|
+
opts.separator "\nOptions:"
|
11
|
+
|
12
|
+
opts.on '-l', '--lang LANG = en', 'Lang for subtitles' do |lang|
|
13
|
+
options[:lang] = lang
|
14
|
+
end
|
15
|
+
|
16
|
+
opts.on '-f', '--force', 'Force subtitles download even if already exists' do |lang|
|
17
|
+
options[:force] = true
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on '-h', '--help', 'Show usage' do
|
21
|
+
puts opts
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
option_parser.parse!
|
27
|
+
|
28
|
+
puts option_parser if ARGV.empty?
|
29
|
+
|
30
|
+
Suby.download_subtitles ARGV, options
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Suby
|
2
|
+
class Downloader::Addic7ed < Downloader
|
3
|
+
SITE = 'www.addic7ed.com'
|
4
|
+
FORMAT = :file
|
5
|
+
SUBTITLE_TYPES = [:tvshow]
|
6
|
+
|
7
|
+
LANG_IDS = {
|
8
|
+
en: 1, es: 5, it: 7, fr: 8, pt: 10, de: 11, ca: 12, eu: 13, cs: 14,
|
9
|
+
gl: 15, tr: 16, nl: 17, sv: 18, ru: 19, hu: 20, pl: 21, sl: 22, he: 23,
|
10
|
+
zh: 24, sk: 25, ro: 26, el: 27, fi: 28, no: 29, da: 30, hr: 31, ja: 32,
|
11
|
+
bg: 35, sr: 36, id: 37, ar: 38, ms: 40, ko: 42, fa: 43, bs: 44, vi: 45,
|
12
|
+
th: 46, bn: 47
|
13
|
+
}
|
14
|
+
FILTER_IGNORED = "Couldn't find any subs with the specified language. " +
|
15
|
+
"Filter ignored"
|
16
|
+
|
17
|
+
def subtitles_url
|
18
|
+
"/serie/#{CGI.escape show}/#{season}/#{episode}/#{LANG_IDS[lang]}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def subtitles_response
|
22
|
+
response = get(subtitles_url, {}, false)
|
23
|
+
unless Net::HTTPSuccess === response
|
24
|
+
raise NotFoundError, "show/season/episode not found"
|
25
|
+
end
|
26
|
+
response
|
27
|
+
end
|
28
|
+
|
29
|
+
def subtitles_body
|
30
|
+
body = subtitles_response.body
|
31
|
+
body.strip!
|
32
|
+
raise NotFoundError, "show/season/episode not found" if body.empty?
|
33
|
+
if body.include? FILTER_IGNORED
|
34
|
+
raise NotFoundError, "no subtitles available"
|
35
|
+
end
|
36
|
+
body
|
37
|
+
end
|
38
|
+
|
39
|
+
def redirected_url download_url
|
40
|
+
header = { 'Referer' => "http://#{SITE}#{subtitles_url}" }
|
41
|
+
response = get download_url, header, false
|
42
|
+
case response
|
43
|
+
when Net::HTTPSuccess
|
44
|
+
response
|
45
|
+
when Net::HTTPFound
|
46
|
+
location = response['Location']
|
47
|
+
if location == '/downloadexceeded.php'
|
48
|
+
raise NotFoundError, "download exceeded"
|
49
|
+
end
|
50
|
+
URI.escape location
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def download_url
|
55
|
+
link = Nokogiri(subtitles_body).css('a').find { |a|
|
56
|
+
a[:href].start_with? '/original/' or
|
57
|
+
a[:href].start_with? '/updated/'
|
58
|
+
}
|
59
|
+
raise NotFoundError, "show/season/episode not found" unless link
|
60
|
+
download_url = link[:href]
|
61
|
+
|
62
|
+
redirected_url download_url
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Suby
|
2
|
+
# Based on https://github.com/byroot/ruby-osdb/blob/master/lib/osdb/server.rb
|
3
|
+
class Downloader::OpenSubtitles < Downloader
|
4
|
+
SITE = 'api.opensubtitles.org'
|
5
|
+
FORMAT = :gz
|
6
|
+
XMLRPC_PATH = '/xml-rpc'
|
7
|
+
SUBTITLE_TYPES = [:tvshow, :movie, :unknown]
|
8
|
+
|
9
|
+
USERNAME = ''
|
10
|
+
PASSWORD = ''
|
11
|
+
LOGIN_LANGUAGE = 'eng'
|
12
|
+
USER_AGENT = 'Suby v0.4'
|
13
|
+
|
14
|
+
SEARCH_QUERIES_ORDER = [:hash, :name] #There is also search using imdbid but i dont think it usefull as it
|
15
|
+
#returns subtitles for many different versions
|
16
|
+
|
17
|
+
# OpenSubtitles needs ISO 639-22B language codes for subtitles search
|
18
|
+
# See http://www.opensubtitles.org/addons/export_languages.php
|
19
|
+
# and http://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
|
20
|
+
LANG_MAPPING = {
|
21
|
+
ar: "ara", bg: "bul", bn: "ben", br: "bre", bs: "bos", ca: "cat", cs: "cze", da: "dan", de: "ger", el: "ell",
|
22
|
+
en: "eng", eo: "epo", es: "spa", et: "est", eu: "baq", fa: "per", fi: "fin", fr: "fre", gl: "glg", he: "heb",
|
23
|
+
hi: "hin", hr: "hrv", hu: "hun", hy: "arm", id: "ind", is: "ice", it: "ita", ja: "jpn", ka: "geo", kk: "kaz",
|
24
|
+
km: "khm", ko: "kor", lb: "ltz", lt: "lit", lv: "lav", mk: "mac", mn: "mon", ms: "may", nl: "dut", no: "nor",
|
25
|
+
oc: "oci", pb: "pob", pl: "pol", pt: "por", ro: "rum", ru: "rus", si: "sin", sk: "slo", sl: "slv", sq: "alb",
|
26
|
+
sr: "scc", sv: "swe", sw: "swa", th: "tha", tl: "tgl", tr: "tur", uk: "ukr", ur: "urd", vi: "vie", zh: "chi"
|
27
|
+
}
|
28
|
+
LANG_MAPPING.default = 'all'
|
29
|
+
|
30
|
+
def download_url
|
31
|
+
SEARCH_QUERIES_ORDER.find(lambda { raise NotFoundError, "no subtitles available" }) { |type|
|
32
|
+
if subs = search_subtitles(search_query(type))['data']
|
33
|
+
@type = type
|
34
|
+
break subs
|
35
|
+
end
|
36
|
+
}.first['SubDownloadLink']
|
37
|
+
end
|
38
|
+
|
39
|
+
def search_subtitles(query)
|
40
|
+
return {} unless query
|
41
|
+
query = [query] unless query.kind_of? Array
|
42
|
+
xmlrpc.call('SearchSubtitles', token, query)
|
43
|
+
end
|
44
|
+
|
45
|
+
def token
|
46
|
+
@token ||= login
|
47
|
+
end
|
48
|
+
|
49
|
+
def login
|
50
|
+
response = xmlrpc.call('LogIn', USERNAME, PASSWORD, LOGIN_LANGUAGE, USER_AGENT)
|
51
|
+
unless response['status'] == '200 OK'
|
52
|
+
raise DownloaderError, "Failed to login with #{USERNAME}:#{PASSWORD}. " +
|
53
|
+
"Server return code: #{response['status']}"
|
54
|
+
end
|
55
|
+
response['token']
|
56
|
+
end
|
57
|
+
|
58
|
+
def search_query(type = :hash)
|
59
|
+
return nil unless query = send("search_query_by_#{type}")
|
60
|
+
query.merge(sublanguageid: language(lang))
|
61
|
+
end
|
62
|
+
|
63
|
+
def search_query_by_hash
|
64
|
+
{ moviehash: MovieHasher.compute_hash(file), moviebytesize: file.size.to_s } if file.exist?
|
65
|
+
end
|
66
|
+
|
67
|
+
def search_query_by_name
|
68
|
+
season && episode ? { query: show, season: season, episode: episode } : { query: file.base.to_s }
|
69
|
+
end
|
70
|
+
|
71
|
+
def search_query_by_imdbid
|
72
|
+
{ imdbid: imdbid } if imdbid
|
73
|
+
end
|
74
|
+
|
75
|
+
def language(lang)
|
76
|
+
LANG_MAPPING[lang.to_sym]
|
77
|
+
end
|
78
|
+
|
79
|
+
def success_message
|
80
|
+
"Found by #{@type}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Suby
|
2
|
+
class Downloader::TVSubtitles < Downloader
|
3
|
+
SITE = 'www.tvsubtitles.net'
|
4
|
+
FORMAT = :zip
|
5
|
+
SEARCH_URL = '/search.php'
|
6
|
+
SUBTITLE_TYPES = [:tvshow]
|
7
|
+
|
8
|
+
# cache
|
9
|
+
SHOW_URLS = {}
|
10
|
+
SHOW_PAGES = {}
|
11
|
+
|
12
|
+
# "Show (2009-2011)" => "Show"
|
13
|
+
def clean_show_name show_name
|
14
|
+
show_name.sub(/ \(\d{4}-\d{4}\)$/, '')
|
15
|
+
end
|
16
|
+
|
17
|
+
def show_url
|
18
|
+
SHOW_URLS[show] ||= begin
|
19
|
+
results = Nokogiri(post(SEARCH_URL, 'q' => show))
|
20
|
+
a = results.css('ul li div a').find { |a|
|
21
|
+
clean_show_name(a.text).casecmp(show) == 0
|
22
|
+
}
|
23
|
+
raise NotFoundError, "show not found" unless a
|
24
|
+
url = a[:href]
|
25
|
+
|
26
|
+
raise 'invalid show url' unless /^\/tvshow-\d+\.html$/ =~ url
|
27
|
+
url
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def season_url
|
32
|
+
show_url.sub(/\.html$/, "-#{season}.html")
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_season?
|
36
|
+
season_text = "Season #{season}"
|
37
|
+
bs = SHOW_PAGES[show].css('div.left_articles p.description b')
|
38
|
+
unless bs.find { |b| b.text == season_text }
|
39
|
+
raise NotFoundError, "season not found"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_episode_row
|
44
|
+
row = SHOW_PAGES[show].css('div.left_articles table tr').find { |tr|
|
45
|
+
tr.children.find { |td| td.name == 'td' and
|
46
|
+
td.text =~ /\A#{season}x0?#{episode}\z/ }
|
47
|
+
}
|
48
|
+
raise NotFoundError, "episode not found" unless row
|
49
|
+
row
|
50
|
+
end
|
51
|
+
|
52
|
+
def episode_url
|
53
|
+
@episode_url ||= begin
|
54
|
+
SHOW_PAGES[show] ||= Nokogiri(get(season_url))
|
55
|
+
has_season?
|
56
|
+
|
57
|
+
url = nil
|
58
|
+
find_episode_row.children.find { |td|
|
59
|
+
td.children.find { |a|
|
60
|
+
a.name == 'a' and a[:href].start_with?('episode') and url = a[:href]
|
61
|
+
}
|
62
|
+
}
|
63
|
+
unless url =~ /^episode-(\d+)\.html$/
|
64
|
+
raise "invalid episode url: #{episode_url}"
|
65
|
+
end
|
66
|
+
|
67
|
+
"/episode-#{$1}-#{lang}.html"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def subtitles_url
|
72
|
+
@subtitles_url ||= begin
|
73
|
+
subtitles = Nokogiri(get(episode_url))
|
74
|
+
|
75
|
+
# TODO: choose 720p or most downloaded instead of first found
|
76
|
+
a = subtitles.css('div.left_articles a').find { |a|
|
77
|
+
a.name == 'a' and a[:href].start_with?('/subtitle')
|
78
|
+
}
|
79
|
+
raise NotFoundError, "no subtitles available" unless a
|
80
|
+
url = a[:href]
|
81
|
+
raise 'invalid subtitle url' unless url =~ /^\/subtitle-(\d+)\.html/
|
82
|
+
url
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def download_url
|
87
|
+
URI.escape '/' + get_redirection(subtitles_url.sub('subtitle', 'download'))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|