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