storyboard 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +3 -0
  2. data/.gitmodules +3 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +55 -0
  6. data/README.md +40 -0
  7. data/TODO +6 -0
  8. data/bin/storyboard +82 -0
  9. data/bin/storyboard-ffprobe +0 -0
  10. data/lib/.DS_Store +0 -0
  11. data/lib/storyboard/generators/pdf.rb +46 -0
  12. data/lib/storyboard/generators/sub.rb +32 -0
  13. data/lib/storyboard/subtitles.rb +96 -0
  14. data/lib/storyboard/thread-util.rb +308 -0
  15. data/lib/storyboard/time.rb +34 -0
  16. data/lib/storyboard/version.rb +3 -0
  17. data/lib/storyboard.rb +119 -0
  18. data/storyboard.gemspec +56 -0
  19. data/vendor/suby/.gitignore +3 -0
  20. data/vendor/suby/LICENSE +19 -0
  21. data/vendor/suby/README.md +27 -0
  22. data/vendor/suby/bin/suby +30 -0
  23. data/vendor/suby/lib/suby/downloader/addic7ed.rb +65 -0
  24. data/vendor/suby/lib/suby/downloader/opensubtitles.rb +83 -0
  25. data/vendor/suby/lib/suby/downloader/tvsubtitles.rb +90 -0
  26. data/vendor/suby/lib/suby/downloader.rb +177 -0
  27. data/vendor/suby/lib/suby/filename_parser.rb +103 -0
  28. data/vendor/suby/lib/suby/interface.rb +17 -0
  29. data/vendor/suby/lib/suby/movie_hasher.rb +31 -0
  30. data/vendor/suby/lib/suby.rb +89 -0
  31. data/vendor/suby/spec/fixtures/.gitkeep +0 -0
  32. data/vendor/suby/spec/mock_http.rb +22 -0
  33. data/vendor/suby/spec/spec_helper.rb +3 -0
  34. data/vendor/suby/spec/suby/downloader/addict7ed_spec.rb +28 -0
  35. data/vendor/suby/spec/suby/downloader/opensubtitles_spec.rb +33 -0
  36. data/vendor/suby/spec/suby/downloader/tvsubtitles_spec.rb +50 -0
  37. data/vendor/suby/spec/suby/downloader_spec.rb +11 -0
  38. data/vendor/suby/spec/suby/filename_parser_spec.rb +66 -0
  39. data/vendor/suby/spec/suby_spec.rb +27 -0
  40. data/vendor/suby/suby.gemspec +20 -0
  41. 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
@@ -0,0 +1,3 @@
1
+ class Storyboard
2
+ VERSION = "0.2.3"
3
+ 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
@@ -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
@@ -0,0 +1,3 @@
1
+ *.gem
2
+ spec/fixtures/*
3
+ !spec/fixtures/.gitkeep
@@ -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