viddl-rb 0.99 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 869f88d6332b09dea9e9c01d3205a1965b9c8c48
4
- data.tar.gz: f208de4df1e7fb57d66e2e005aee7f1dbda147f6
3
+ metadata.gz: aff24988a745ce18c4928aca3c4e50fb47232f1d
4
+ data.tar.gz: c0fff623ca863b24999582c94d582a2de1288c1f
5
5
  SHA512:
6
- metadata.gz: c656fa95771d40f473c698a46fa6395129b6ad5100794479a245d1ade7c51d7d0131077baae825ecf120925abc67c62a24a3924d0470b91b26e5b92d8b165784
7
- data.tar.gz: 01be0e2e4510fc211f8661999e5d34f2e74c4582a57ce05619babb2f80b9c48c34a448c669e5f873db5ef250e1f27789932e658e3400ac4fd03019c32dd4c404
6
+ metadata.gz: 04af8ad42b49b38d890fb2e1536a2e8de0573c6a419c984bb3135078c18cc98a1f3fd9c760d885c540372ebdbf303d164147f240e3ca760edf0161bac16042b7
7
+ data.tar.gz: 9a7b77633e663298a5ea5872afef78491de249906ff598d73401c15e9f2db4ba99635118e83e74fcebb61ca65da30d59b0955b03846595fd9720c4b70d357b63
data/Rakefile CHANGED
@@ -2,12 +2,13 @@ require 'rubygems'
2
2
  require 'bundler/setup'
3
3
  require 'rake/testtask'
4
4
 
5
- ALL_INTEGRATION = FileList["spec/integration/*.rb", "spec/integration/*/*.rb"]
6
- ALL_UNIT = FileList["spec/unit/*/*.rb"]
5
+ SKIPPED_INTEGRATION = FileList["spec/integration/youtube/cipher_guesser_spec.rb"]
6
+ ALL_INTEGRATION = FileList["spec/integration/*.rb", "spec/integration/*/*.rb"] - SKIPPED_INTEGRATION
7
+ ALL_UNIT = FileList["spec/unit/*.rb", "spec/unit/*/*.rb"]
7
8
 
8
- task :default => [:all]
9
+ task :default => [:test_all]
9
10
 
10
- Rake::TestTask.new(:all) do |t|
11
+ Rake::TestTask.new(:test_all) do |t|
11
12
  t.test_files = ALL_INTEGRATION + ALL_UNIT
12
13
  end
13
14
 
@@ -34,3 +35,11 @@ end
34
35
  Rake::TestTask.new(:test_cipher_loader) do |t|
35
36
  t.test_files = FileList["spec/integration/youtube/cipher_loader_spec.rb"]
36
37
  end
38
+
39
+ Rake::TestTask.new(:test_cipher_guesser) do |t|
40
+ t.test_files = FileList["spec/unit/youtube/cipher_guesser_spec.rb"]
41
+ end
42
+
43
+ Rake::TestTask.new(:test_decipherer) do |t|
44
+ t.test_files = FileList["spec/unit/youtube/decipherer_spec.rb"]
45
+ end
@@ -15,15 +15,17 @@ class Downloader
15
15
  name,
16
16
  :save_dir => params[:save_dir],
17
17
  :tool => params[:tool] && params[:tool].to_sym
18
- unless result
18
+ if result
19
+ puts "Download for #{name} successful."
20
+ url_name[:on_downloaded].call(true) if url_name[:on_downloaded]
21
+ ViddlRb::AudioHelper.extract(name, params[:save_dir]) if params[:extract_audio]
22
+ else
23
+ url_name[:on_downloaded].call(false) if url_name[:on_downloaded]
19
24
  if params[:abort_on_failure]
20
25
  raise DownloadFailedError, "Download for #{name} failed."
21
26
  else
22
27
  puts "Download for #{name} failed. Moving onto next file."
23
28
  end
24
- else
25
- puts "Download for #{name} successful."
26
- ViddlRb::AudioHelper.extract(name, params[:save_dir]) if params[:extract_audio]
27
29
  end
28
30
  end
29
31
  end
data/bin/viddl-rb CHANGED
@@ -28,14 +28,14 @@ begin
28
28
  puts "Will try to extract audio: #{params[:extract_audio] == true}."
29
29
  puts "Analyzing URL: #{params[:url]}"
30
30
 
31
- app = Driver.new(params)
32
- app.start # starts the download process
31
+ driver = Driver.new(params)
32
+ driver.start # starts the download process
33
33
 
34
34
  rescue OptionParser::ParseError, ViddlRb::RequirementError => e
35
35
  puts "Error: #{e.message}"
36
36
  exit(1)
37
37
 
38
- rescue StandardError => e
38
+ rescue => e
39
39
  puts "Error: #{e.message}"
40
40
  puts "\nBacktrace:"
41
41
  puts e.backtrace
@@ -52,27 +52,28 @@ module ViddlRb
52
52
  File.join(File.dirname(File.expand_path(__FILE__)), "..")
53
53
  end
54
54
 
55
- #checks to see whether the os has a certain utility like wget or curl
56
- #`` returns the standard output of the process
57
- #system returns the exit code of the process
55
+ # checks to see whether the os has a certain utility like wget or curl
56
+ # `` returns the standard output of the process
57
+ # system returns the exit code of the process
58
58
  def self.os_has?(utility)
59
-
60
- unless windows?
61
- `which #{utility}`.include?(utility.to_s)
62
- else
59
+ if windows?
63
60
  if !system("where /q where").nil? #if Windows has the where utility
64
61
  system("where /q #{utility}") #/q is the quiet mode flag
65
62
  else
66
- begin #as a fallback we just run the utility itself
63
+ begin #as a fallback we just run the utility itself
67
64
  system(utility)
68
65
  rescue Errno::ENOENT
69
66
  false
70
67
  end
71
68
  end
69
+ else
70
+ # This might work in windows too... I am not quite sure :-/
71
+ ENV['PATH'].split(':').any?{|dir| File.exist?( File.join(dir, utility.to_s) ) }
72
72
  end
73
73
  end
74
74
 
75
- #recursively get the final location (after following all redirects) for an url.
75
+ # recursively get the final location (after following all redirects)
76
+ # for an url.
76
77
  def self.get_final_location(url)
77
78
  Net::HTTP.get_response(URI.parse(url)) do |res|
78
79
  location = res["location"]
data/plugins/blip.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  class Blip < PluginBase
3
2
  # this will be called by the main app to check whether this plugin is responsible for the url passed
4
3
  def self.matches_provider?(url)
data/plugins/vimeo.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  class Vimeo < PluginBase
3
2
 
4
3
  #this will be called by the main app to check whether this plugin is responsible for the url passed
data/plugins/youtube.rb CHANGED
@@ -1,18 +1,16 @@
1
-
2
1
  class Youtube < PluginBase
3
2
 
3
+ #TODO: TEST THIS: https://www.youtube.com/watch?v=Qapou-3-fM8&list=PL_Z529zmzNGcOVBJA0MgjjQoKiBcmMQWh
4
+
4
5
  # this will be called by the main app to check whether this plugin is responsible for the url passed
5
6
  def self.matches_provider?(url)
6
7
  url.include?("youtube.com") || url.include?("youtu.be")
7
8
  end
8
9
 
9
10
  def self.get_urls_and_filenames(url, options = {})
11
+ initialize_components(options)
10
12
 
11
- @url_resolver = UrlResolver.new
12
- @video_resolver = VideoResolver.new(Decipherer.new(CipherLoader.new))
13
- @format_picker = FormatPicker.new(options)
14
-
15
- urls = @url_resolver.get_all_urls(url, options[:filter])
13
+ urls = @url_resolver.get_all_urls(url, options[:filter])
16
14
  videos = get_videos(urls)
17
15
 
18
16
  return_value = videos.map do |video|
@@ -23,6 +21,14 @@ class Youtube < PluginBase
23
21
  return_value.empty? ? download_error("No videos could be downloaded.") : return_value
24
22
  end
25
23
 
24
+ def self.initialize_components(options)
25
+ @cipher_io = CipherIO.new
26
+ coordinator = DecipherCoordinator.new(Decipherer.new(@cipher_io), CipherGuesser.new)
27
+ @video_resolver = VideoResolver.new(coordinator)
28
+ @url_resolver = UrlResolver.new
29
+ @format_picker = FormatPicker.new(options)
30
+ end
31
+
26
32
  def self.notify(message)
27
33
  puts "[YOUTUBE] #{message}"
28
34
  end
@@ -37,17 +43,26 @@ class Youtube < PluginBase
37
43
  @video_resolver.get_video(url)
38
44
  rescue VideoResolver::VideoRemovedError
39
45
  notify "The video #{url} has been removed."
46
+ nil
40
47
  rescue => e
41
48
  notify "Error getting the video: #{e.message}"
49
+ nil
42
50
  end
43
51
  end
44
-
45
52
  videos.reject(&:nil?)
46
53
  end
47
54
 
48
55
  def self.make_url_filname_hash(video, format)
49
56
  url = video.get_download_url(format.itag)
50
57
  name = PluginBase.make_filename_safe(video.title) + ".#{format.extension}"
51
- {url: url, name: name}
58
+ {url: url, name: name, on_downloaded: make_downloaded_callback(video)}
59
+ end
60
+
61
+ def self.make_downloaded_callback(video)
62
+ return nil unless video.signature_guess?
63
+
64
+ lambda do |success|
65
+ @cipher_io.add_cipher(video.cipher_version, video.cipher_operations) if success
66
+ end
52
67
  end
53
68
  end
@@ -0,0 +1,114 @@
1
+ require 'open-uri'
2
+
3
+ class CipherGuesser
4
+ class CipherGuessError < StandardError; end
5
+
6
+ JS_URL = "http://s.ytimg.com/yts/jsbin/html5player-%s.js"
7
+
8
+ def guess(cipher_version)
9
+ js = download_player_javascript(cipher_version)
10
+ body = extract_decipher_function_body(js)
11
+
12
+ parse_function_body(body)
13
+ end
14
+
15
+ private
16
+
17
+ def download_player_javascript(cipher_version)
18
+ open(JS_URL % cipher_version).read
19
+ end
20
+
21
+ def extract_decipher_function_body(js)
22
+ function_name = js[decipher_function_name_regex, 1]
23
+ function_regex = get_function_regex(function_name)
24
+ match = function_regex.match(js)
25
+
26
+ raise(CipherGuessError, "Could not extract the decipher function") unless match
27
+ match[:brace]
28
+ end
29
+
30
+ def parse_function_body(body)
31
+ lines = body.split(";")
32
+
33
+ remove_non_decipher_lines!(lines)
34
+ do_pre_transformations!(lines)
35
+
36
+ lines.map do |line|
37
+ if /\(\w+,(?<index>\d+)\)/ =~ line # calling a two argument function (swap)
38
+ "w#{index}"
39
+ elsif /slice\((?<index>\d+)\)/ =~ line # calling slice
40
+ "s#{index}"
41
+ elsif /reverse\(\)/ =~ line # calling reverse
42
+ "r"
43
+ else
44
+ raise "Cannot parse line: #{line}"
45
+ end
46
+ end
47
+ end
48
+
49
+ def remove_non_decipher_lines!(lines)
50
+ # The first line splits the string into an array and the last joins and returns
51
+ lines.delete_at(0)
52
+ lines.delete_at(-1)
53
+ end
54
+
55
+ def do_pre_transformations!(lines)
56
+ change_inline_swap_to_function_call(lines) if inline_swap?(lines)
57
+ end
58
+
59
+ def inline_swap?(lines)
60
+ # Defining a variable = inline swap function
61
+ lines.any? { |line| line.include?("var ") }
62
+ end
63
+
64
+ def change_inline_swap_to_function_call(lines)
65
+ start_index = lines.find_index { |line| line.include?("var ") }
66
+ swap_lines = lines.slice!(start_index, 3) # inline swap is 3 lines long
67
+ i1, i2 = get_swap_indices(swap_lines)
68
+
69
+ lines.insert(start_index, "swap(#{i1},#{i2})")
70
+ lines
71
+ end
72
+
73
+ def get_swap_indices(lines)
74
+ i1 = lines.first[/(\d+)/, 1]
75
+ i2 = lines.last[/(\d+)/, 1]
76
+ [i1, i2]
77
+ end
78
+
79
+ def decipher_function_name_regex
80
+ # Find "C" in this: var A = B.sig || C (B.s)
81
+ /
82
+ \.sig
83
+ \s*
84
+ \|\|
85
+ (\w+)
86
+ \(
87
+ /x
88
+ end
89
+
90
+ def get_function_regex(function_name)
91
+ # Match the function function_name (that has one argument)
92
+ /
93
+ #{function_name}
94
+ \(
95
+ \w+
96
+ \)
97
+ #{function_body_regex}
98
+ /x
99
+ end
100
+
101
+ def function_body_regex
102
+ # Match nested braces
103
+ /
104
+ (?<brace>
105
+ {
106
+ (
107
+ [^{}]
108
+ | \g<brace>
109
+ )*
110
+ }
111
+ )
112
+ /x
113
+ end
114
+ end
@@ -4,7 +4,7 @@ require 'net/http'
4
4
  require 'openssl'
5
5
  require 'yaml'
6
6
 
7
- class CipherLoader
7
+ class CipherIO
8
8
 
9
9
  CIPHER_YAML_URL = "https://raw.github.com/rb2k/viddl-rb/master/plugins/youtube/ciphers.yml"
10
10
  CIPHER_YAML_PATH = File.join(ViddlRb::UtilityHelper.base_path, "plugins/youtube/ciphers.yml")
@@ -23,24 +23,29 @@ class CipherLoader
23
23
  @ciphers.dup
24
24
  end
25
25
 
26
+ def add_cipher(version, operations)
27
+ File.open(CIPHER_YAML_PATH, "a") do |file|
28
+ file.puts("#{version}: #{operations}")
29
+ end
30
+ end
31
+
26
32
  private
27
33
 
28
34
  def update_ciphers
29
- return if get_server_file_size == get_local_file_size
35
+ server_etag = get_server_etag
36
+ return if server_etag == @ciphers["ETag"]
30
37
 
31
38
  @ciphers.merge!(download_server_ciphers)
39
+ @ciphers["ETag"] = server_etag
32
40
  save_local_ciphers(@ciphers)
33
41
  end
34
42
 
35
- def get_local_file_size
36
- File.size(CIPHER_YAML_PATH)
37
- end
38
-
39
- def get_server_file_size
43
+ def get_server_etag
40
44
  uri = URI.parse(CIPHER_YAML_URL)
41
45
  http = make_http(uri)
42
46
  head = Net::HTTP::Head.new(uri.request_uri)
43
- http.request(head)["Content-Length"].to_i
47
+ etag = http.request(head)["ETag"]
48
+ etag.gsub('"', '') # remove leading and trailing quotes
44
49
  end
45
50
 
46
51
  def make_http(uri)
@@ -84,3 +84,16 @@ ima-vflxBu-5R: w40 w62 r s2 w21 s3 r w7 s3
84
84
  ima-vflrGwWV9: w36 w45 r s2 r
85
85
  ima-vflCME3y0: w8 s2 r w52
86
86
  ima-vfl1LZyZ5: w8 s2 r w52
87
+ ima-vfl4_saJa: r s1 w19 w9 w57 w38 s3 r s2
88
+ ima-en_US-vflP9269H: r w63 w37 s3 r w14 r
89
+ ima-en_US-vflkClbFb: s1 w12 w24 s1 w52 w70 s2
90
+ ima-en_US-vflYhChiG: w27 r s3
91
+ ima-en_US-vflWnCYSF: r s1 r s3 w19 r w35 w61 s2
92
+ en_US-vflbT9-GA: w51 w15 s1 w22 s1 w41 r w43 r
93
+ en_US-vflAYBrl7: s2 r w39 w43
94
+ en_US-vflS1POwl: w48 s2 r s1 w4 w35
95
+ en_US-vflLMtkhg: w30 r w30 w39
96
+ en_US-vflbJnZqE: w26 s1 w15 w3 w62 w54 w22
97
+ en_US-vflgd5txb: w26 s1 w15 w3 w62 w54 w22
98
+ en_US-vflTm330y: w26 s1 w15 w3 w62 w54 w22
99
+ en_US-vflnwMARr: s3 r w24 s2
@@ -0,0 +1,28 @@
1
+
2
+ class DecipherCoordinator
3
+
4
+ def initialize(decipherer, cipher_guesser)
5
+ @decipherer = decipherer
6
+ @cipher_guesser = cipher_guesser
7
+ end
8
+
9
+ def get_decipher_data(cipher_version)
10
+ ops = @decipherer.get_operations(cipher_version)
11
+ Youtube.notify "Cipher guess: no"
12
+ {version: cipher_version, operations: ops.join(" "), guess?: false}
13
+
14
+ rescue Decipherer::UnknownCipherVersionError => e
15
+ ops = @cipher_guesser.guess(cipher_version)
16
+ Youtube.notify "Cipher guess: yes"
17
+ {version: cipher_version, operations: ops.join(" "), guess?: true}
18
+
19
+ rescue Decipherer::UnknownCipherOperationError => e
20
+ Youtube.notify "Failed to parse the cipher from the Youtube player version #{cipher_version}\n" +
21
+ "Please submit a bug report at https://github.com/rb2k/viddl-rb"
22
+ raise e
23
+ end
24
+
25
+ def decipher_with_operations(cipher, operations)
26
+ @decipherer.decipher_with_operations(cipher, operations)
27
+ end
28
+ end
@@ -1,22 +1,36 @@
1
1
 
2
2
  class Decipherer
3
3
 
4
- class UnknownCipherVersionError < StandardError; end
5
4
  class UnknownCipherOperationError < StandardError; end
6
5
 
6
+ class UnknownCipherVersionError < StandardError
7
+
8
+ attr_reader :cipher_version
9
+
10
+ def initialize(cipher_version)
11
+ super("Unknown cipher version: #{cipher_version}")
12
+ @cipher_version = cipher_version
13
+ end
14
+ end
15
+
7
16
  def initialize(loader)
8
17
  @ciphers = loader.load_ciphers
9
18
  end
10
19
 
11
20
  def decipher_with_version(cipher, cipher_version)
12
- operations = @ciphers[cipher_version]
13
- raise UnknownCipherVersionError.new("Unknown cipher version: #{cipher_version}") unless operations
21
+ ops = get_operations(cipher_version)
22
+ decipher_with_operations(cipher, ops)
23
+ end
14
24
 
15
- decipher_with_operations(cipher, operations.split)
25
+ def get_operations(cipher_version)
26
+ operations = @ciphers[cipher_version]
27
+ raise UnknownCipherVersionError.new(cipher_version) unless operations
28
+ operations.split
16
29
  end
17
30
 
18
31
  def decipher_with_operations(cipher, operations)
19
32
  cipher = cipher.dup
33
+ operations = operations.split if operations.is_a?(String)
20
34
 
21
35
  operations.each do |op|
22
36
  cipher = apply_operation(cipher, op)
@@ -110,7 +110,7 @@ class FormatPicker
110
110
  when 1
111
111
  formats.first
112
112
  else
113
- get_default_format(matches_resolution)
113
+ get_default_format(formats)
114
114
  end
115
115
  end
116
116
  end
@@ -11,8 +11,13 @@ class VideoResolver
11
11
  end
12
12
 
13
13
  def get_video(url)
14
- @json = load_json(url)
15
- Video.new(get_title, parse_stream_map(get_stream_map))
14
+ @json = load_json(url)
15
+ decipher_data = @decipherer.get_decipher_data(get_html5player_version)
16
+ url_data = parse_stream_map(get_stream_map)
17
+
18
+ decipher_signatures!(url_data, decipher_data)
19
+
20
+ Video.new(get_title, url_data, decipher_data)
16
21
  end
17
22
 
18
23
  private
@@ -47,10 +52,7 @@ class VideoResolver
47
52
  #
48
53
  def parse_stream_map(stream_map)
49
54
  entries = stream_map.split(",")
50
-
51
- parsed = entries.map { |entry| parse_stream_map_entry(entry) }
52
- parsed.each { |entry| apply_signature!(entry) if entry[:sig] }
53
- parsed
55
+ entries.map { |entry| parse_stream_map_entry(entry) }
54
56
  end
55
57
 
56
58
  def parse_stream_map_entry(entry)
@@ -65,7 +67,7 @@ class VideoResolver
65
67
  end
66
68
 
67
69
  # The signature key can be either "sig" or "s".
68
- # Very rarely there is no "s" or "sig" paramater. In this case the signature is already
70
+ # Very rarely there is no "s" or "sig" parameter. In this case the signature is already
69
71
  # applied and the the video can be downloaded directly.
70
72
  def fetch_signature(params)
71
73
  sig = params.fetch("sig", nil) || params.fetch("s", nil)
@@ -79,32 +81,45 @@ class VideoResolver
79
81
  text
80
82
  end
81
83
 
82
- def apply_signature!(entry)
83
- sig = get_deciphered_sig(entry[:sig])
84
- entry[:url] << "&#{SIGNATURE_URL_PARAMETER}=#{sig}"
85
- entry.delete(:sig)
86
- end
84
+ def decipher_signatures!(url_data, decipher_data)
85
+ url_data.each do |entry|
86
+ next unless entry[:sig]
87
87
 
88
- def get_deciphered_sig(sig)
89
- return sig if sig.length == CORRECT_SIGNATURE_LENGTH
90
- @decipherer.decipher_with_version(sig, get_html5player_version)
88
+ sig = @decipherer.decipher_with_operations(entry[:sig], decipher_data[:operations])
89
+ entry[:url] << "&#{SIGNATURE_URL_PARAMETER}=#{sig}"
90
+ entry.delete(:sig)
91
+ end
91
92
  end
92
93
 
94
+
93
95
  class Video
94
96
  attr_reader :title
95
97
 
96
- def initialize(title, itags_urls)
98
+ def initialize(title, url_data, decipher_data)
97
99
  @title = title
98
- @itags_urls = itags_urls
100
+ @url_data = url_data
101
+ @decipher_data = decipher_data
99
102
  end
100
103
 
101
104
  def available_itags
102
- @itags_urls.map { |iu| iu[:itag] }
105
+ @url_data.map { |entry| entry[:itag] }
103
106
  end
104
107
 
105
108
  def get_download_url(itag)
106
- itag_url = @itags_urls.find { |iu| iu[:itag] == itag }
107
- itag_url[:url] if itag_url
109
+ entry = @url_data.find { |entry| entry[:itag] == itag }
110
+ entry[:url] if entry
111
+ end
112
+
113
+ def signature_guess?
114
+ @decipher_data[:guess?]
115
+ end
116
+
117
+ def cipher_operations
118
+ @decipher_data[:operations]
119
+ end
120
+
121
+ def cipher_version
122
+ @decipher_data[:version]
108
123
  end
109
124
  end
110
125
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: viddl-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.99"
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc Seeger
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2014-03-02 00:00:00 Z
12
+ date: 2014-04-07 00:00:00 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mime-types
@@ -95,8 +95,10 @@ files:
95
95
  - plugins/soundcloud.rb
96
96
  - plugins/veoh.rb
97
97
  - plugins/vimeo.rb
98
- - plugins/youtube/cipher_loader.rb
98
+ - plugins/youtube/cipher_guesser.rb
99
+ - plugins/youtube/cipher_io.rb
99
100
  - plugins/youtube/ciphers.yml
101
+ - plugins/youtube/decipher_coordinator.rb
100
102
  - plugins/youtube/decipherer.rb
101
103
  - plugins/youtube/format_picker.rb
102
104
  - plugins/youtube/url_resolver.rb