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