viddl-rb 0.95 → 0.96
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +3 -1
- data/Rakefile +18 -8
- data/bin/helper/driver.rb +1 -1
- data/bin/helper/parameter-parser.rb +35 -16
- data/helper/plugin-helper.rb +0 -2
- data/helper/utility-helper.rb +22 -4
- data/plugins/youtube.rb +23 -288
- data/plugins/youtube/decipherer.rb +149 -0
- data/plugins/youtube/format_picker.rb +116 -0
- data/plugins/youtube/url_resolver.rb +77 -0
- data/plugins/youtube/video_resolver.rb +111 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz: 6b2cb672ebfa65f7f7231fdd615a77287757a529
|
4
|
-
data.tar.gz: 03441807280152b0f821e648bec6289862aa7a60
|
5
2
|
SHA512:
|
6
|
-
|
7
|
-
|
3
|
+
data.tar.gz: ff67e34025fae7f2799db8f7fcde11485942e5a7971d8f915bff66ec9ed49d20a0d8013f14268765dade6adc24c6c80320306fef363b78c876076942071d11eb
|
4
|
+
metadata.gz: d77571b6da60ad4fdb6c235232c19c528ce7e3f03f290c580d89953ee4dc24d04d77fc02d9e5422007c4902a8263fa0c5fccf47e5329ca37c9970fa82ccbd462
|
5
|
+
SHA1:
|
6
|
+
data.tar.gz: fafe08cbf9074f38e92dcb632f397e45f86d2c00
|
7
|
+
metadata.gz: 2019cc2be56479dd8827a2979641b8b14bc6cd08
|
data/README.md
CHANGED
@@ -23,7 +23,9 @@ Viddl-rb supports the following command line options:
|
|
23
23
|
-f, --filter REGEX Filters a video playlist according to the regex (Youtube only right now)
|
24
24
|
-s, --save-dir DIRECTORY Specifies the directory where videos should be saved
|
25
25
|
-d, --downloader TOOL Specifies the tool to download with. Supports 'wget', 'curl' and 'net-http'
|
26
|
-
-q, --quality QUALITY Specifies the video format and resolution in the following way
|
26
|
+
-q, --quality QUALITY Specifies the video format and resolution in the following way: width:height:res (e.g. 1280:720:mp4)
|
27
|
+
The width, height and resolution may be omitted with a *.
|
28
|
+
For example, to match any quality with a width of 720 pixels in any format specify --quality *:720:*
|
27
29
|
-h, --help Displays the help screen
|
28
30
|
```
|
29
31
|
|
data/Rakefile
CHANGED
@@ -2,21 +2,31 @@ require 'rubygems'
|
|
2
2
|
require 'bundler/setup'
|
3
3
|
require 'rake/testtask'
|
4
4
|
|
5
|
-
|
5
|
+
ALL_INTEGRATION = FileList["spec/integration/*.rb"]
|
6
|
+
ALL_UNIT = FileList["spec/unit/*/*.rb"]
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
task :default => [:all]
|
9
|
+
|
10
|
+
Rake::TestTask.new(:all) do |t|
|
11
|
+
t.test_files = ALL_INTEGRATION + ALL_UNIT
|
12
|
+
end
|
13
|
+
|
14
|
+
Rake::TestTask.new(:test_unit) do |t|
|
15
|
+
t.test_files = ALL_UNIT
|
16
|
+
end
|
17
|
+
|
18
|
+
Rake::TestTask.new(:test_integration) do |t|
|
19
|
+
t.test_files = ALL_INTEGRATION
|
10
20
|
end
|
11
21
|
|
12
22
|
Rake::TestTask.new(:test_lib) do |t|
|
13
|
-
t.test_files = FileList["spec/lib_spec.rb"]
|
23
|
+
t.test_files = FileList["spec/integration/lib_spec.rb"]
|
14
24
|
end
|
15
25
|
|
16
26
|
Rake::TestTask.new(:test_extract) do |t|
|
17
|
-
t.test_files = FileList["spec/url_extraction_spec.rb"]
|
27
|
+
t.test_files = FileList["spec/integration/url_extraction_spec.rb"]
|
18
28
|
end
|
19
29
|
|
20
|
-
Rake::TestTask.new(:
|
21
|
-
t.test_files = FileList["spec/
|
30
|
+
Rake::TestTask.new(:test_download) do |t|
|
31
|
+
t.test_files = FileList["spec/integration/download_spec.rb"]
|
22
32
|
end
|
data/bin/helper/driver.rb
CHANGED
@@ -36,7 +36,7 @@ class Driver
|
|
36
36
|
plugin.get_urls_and_filenames(url, @params)
|
37
37
|
|
38
38
|
rescue ViddlRb::PluginBase::CouldNotDownloadVideoError => e
|
39
|
-
raise "
|
39
|
+
raise "CouldNotDownloadVideoError.\n" +
|
40
40
|
"Reason: #{e.message}"
|
41
41
|
rescue StandardError => e
|
42
42
|
raise "Error while running the #{plugin.name.inspect} plugin. Maybe it has to be updated?\n" +
|
@@ -34,6 +34,10 @@ class ParameterParser
|
|
34
34
|
optparse = OptionParser.new do |opts|
|
35
35
|
opts.banner = "Usage: viddl-rb URL [options]"
|
36
36
|
|
37
|
+
opts.on('-h', '--help', 'Display this screen') do
|
38
|
+
print_help_and_exit(opts)
|
39
|
+
end
|
40
|
+
|
37
41
|
opts.on("-e", "--extract-audio", "Save video audio to file") do
|
38
42
|
if ViddlRb::UtilityHelper.os_has?("ffmpeg")
|
39
43
|
options[:extract_audio] = true
|
@@ -75,21 +79,18 @@ class ParameterParser
|
|
75
79
|
end
|
76
80
|
|
77
81
|
opts.on("-q", "--quality QUALITY",
|
78
|
-
"Specifies the video format and resolution in the following way
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
opts.on_tail('-h', '--help', 'Display this screen') do
|
92
|
-
print_help_and_exit(opts)
|
82
|
+
"Specifies the video format and resolution in the following way: width:height:res (e.g. 1280:720:mp4). " +
|
83
|
+
"The width, height and resolution may be omitted with a *. For example, to match any quality with a " +
|
84
|
+
"width of 720 pixels in any format specify --quality *:720:*") do |quality|
|
85
|
+
|
86
|
+
tokens = quality.split(":")
|
87
|
+
raise OptionParser::InvalidArgument.new("#{quality} is not a valid argument.") unless tokens.size == 3
|
88
|
+
width, height, ext = tokens
|
89
|
+
validate_quality_options!(width, height, ext, quality)
|
90
|
+
|
91
|
+
options[:quality] = {width: width == "*" ? nil : width.to_i,
|
92
|
+
height: height == "*" ? nil : height.to_i,
|
93
|
+
extension: ext == "*" ? nil : ext}
|
93
94
|
end
|
94
95
|
end
|
95
96
|
|
@@ -109,7 +110,25 @@ class ParameterParser
|
|
109
110
|
def self.validate_url!(url)
|
110
111
|
unless url =~ /^http/
|
111
112
|
raise OptionParser::InvalidArgument.new(
|
112
|
-
|
113
|
+
"please include 'http' with your URL e.g. http://www.youtube.com/watch?v=QH2-TGUlwu4")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.validate_quality_options!(width, height, extension, quality)
|
118
|
+
if !width =~ /(\*|\d+)/ || !height =~ /(\*|\d+)/ || !extension =~ /(\*|\w+)/
|
119
|
+
raise OptionParser::InvalidArgument.new("#{quality} is not a valid argument.")
|
113
120
|
end
|
114
121
|
end
|
115
122
|
end
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
|
data/helper/plugin-helper.rb
CHANGED
data/helper/utility-helper.rb
CHANGED
@@ -3,11 +3,29 @@
|
|
3
3
|
module ViddlRb
|
4
4
|
|
5
5
|
class UtilityHelper
|
6
|
-
|
7
|
-
#
|
6
|
+
|
7
|
+
# Loads all plugins in the plugin directory.
|
8
|
+
# The plugin classes are dynamically added to the ViddlRb module.
|
9
|
+
# A plugin can have helper classes. These classes must exist in a in directory under the
|
10
|
+
# plugins directory that has the same name as the plugin filename wihouth the .rb extension.
|
11
|
+
# All classes found in such a directory will dynamically added as inner classes of the
|
12
|
+
# plugin class.
|
8
13
|
def self.load_plugins
|
9
|
-
|
10
|
-
|
14
|
+
plugins_dir = File.join(File.dirname(__FILE__), "../plugins")
|
15
|
+
plugin_paths = Dir[File.join(plugins_dir, "*.rb")]
|
16
|
+
|
17
|
+
plugin_paths.each do |path|
|
18
|
+
filename = File.basename(path, File.extname(path))
|
19
|
+
plugin_code = File.read(path)
|
20
|
+
class_name = plugin_code[/class (\w+) < PluginBase/, 1]
|
21
|
+
components = Dir[File.join(plugins_dir, filename, "*.rb")]
|
22
|
+
|
23
|
+
ViddlRb.class_eval(plugin_code)
|
24
|
+
|
25
|
+
components.each do |component|
|
26
|
+
code = File.read(component)
|
27
|
+
ViddlRb.const_get(class_name).class_eval(code)
|
28
|
+
end
|
11
29
|
end
|
12
30
|
end
|
13
31
|
|
data/plugins/youtube.rb
CHANGED
@@ -1,231 +1,26 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
1
|
|
3
2
|
class Youtube < PluginBase
|
4
3
|
|
5
|
-
# see http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
|
6
|
-
# TODO: we don't have all the formats from the wiki article here
|
7
|
-
VIDEO_FORMATS = {
|
8
|
-
"38" => {:extension => "mp4", :name => "MP4 Highest Quality 4096x3027 (H.264, AAC)"},
|
9
|
-
"37" => {:extension => "mp4", :name => "MP4 Highest Quality 1920x1080 (H.264, AAC)"},
|
10
|
-
"22" => {:extension => "mp4", :name => "MP4 1280x720 (H.264, AAC)"},
|
11
|
-
"46" => {:extension => "webm", :name => "WebM 1920x1080 (VP8, Vorbis)"},
|
12
|
-
"45" => {:extension => "webm", :name => "WebM 1280x720 (VP8, Vorbis)"},
|
13
|
-
"44" => {:extension => "webm", :name => "WebM 854x480 (VP8, Vorbis)"},
|
14
|
-
"43" => {:extension => "webm", :name => "WebM 480x360 (VP8, Vorbis)"},
|
15
|
-
"18" => {:extension => "mp4", :name => "MP4 640x360 (H.264, AAC)"},
|
16
|
-
"35" => {:extension => "flv", :name => "FLV 854x480 (H.264, AAC)"},
|
17
|
-
"34" => {:extension => "flv", :name => "FLV 640x360 (H.264, AAC)"},
|
18
|
-
"6" => {:extension => "flv", :name => "FLV 640x360 (Soerenson H.263)"},
|
19
|
-
"5" => {:extension => "flv", :name => "FLV 400x240 (Soerenson H.263)"},
|
20
|
-
"36" => {:extension => "3gp", :name => "3gp Medium Quality - 320x240 (MPEG-4 Visual, AAC)"},
|
21
|
-
"17" => {:extension => "3gp", :name => "3gp Medium Quality - 176x144 (MPEG-4 Visual, AAC)"},
|
22
|
-
"13" => {:extension => "3gp", :name => "3gp Low Quality - 176x144 (MPEG-4 Visual, AAC)"},
|
23
|
-
"82" => {:extension => "mp4", :name => "MP4 360p (H.264 AAC)"},
|
24
|
-
"83" => {:extension => "mp4", :name => "MP4 240p (H.264 AAC)"},
|
25
|
-
"84" => {:extension => "mp4", :name => "MP4 720p (H.264 AAC)"},
|
26
|
-
"85" => {:extension => "mp4", :name => "MP4 520p (H.264 AAC)"},
|
27
|
-
"100" => {:extension => "webm", :name => "WebM 360p (VP8 Vorbis)"},
|
28
|
-
"101" => {:extension => "webm", :name => "WebM 360p (VP8 Vorbis)"},
|
29
|
-
"102" => {:extension => "webm", :name => "WebM 720p (VP8 Vorbis)"},
|
30
|
-
"120" => {:extension => "flv", :name => "FLV 720p (H.264 AAC)"},
|
31
|
-
"133" => {:extension => "mp4", :name => "MP4 240p (H.264)"},
|
32
|
-
"134" => {:extension => "mp4", :name => "MP4 360p (H.264)"},
|
33
|
-
"135" => {:extension => "mp4", :name => "MP4 480p (H.264)"},
|
34
|
-
"136" => {:extension => "mp4", :name => "MP4 720p (H.264)"},
|
35
|
-
"137" => {:extension => "mp4", :name => "MP4 1080p (H.264)"},
|
36
|
-
"139" => {:extension => "mp4", :name => "MP4 (AAC)"},
|
37
|
-
"140" => {:extension => "mp4", :name => "MP4 (AAC"},
|
38
|
-
"141" => {:extension => "mp4", :name => "MP4 (AAC)"},
|
39
|
-
"160" => {:extension => "mp4", :name => "MP4 (H.264)"},
|
40
|
-
"171" => {:extension => "webm", :name => "WebM (Vorbis)"},
|
41
|
-
"172" => {:extension => "webm", :name => "WebM (Vorbis)"}
|
42
|
-
}
|
43
|
-
|
44
|
-
DEFAULT_FORMAT_ORDER = %w[38 37 22 46 45 44 43 18 35 34 6 5 36 17 13 82 83 84 85 100 101 102 120 133 134 135 136 137 139 140 141 160 171 172]
|
45
|
-
VIDEO_INFO_URL = "http://www.youtube.com/get_video_info?video_id="
|
46
|
-
VIDEO_INFO_PARMS = "&ps=default&eurl=&gl=US&hl=en"
|
47
|
-
|
48
4
|
# this will be called by the main app to check whether this plugin is responsible for the url passed
|
49
5
|
def self.matches_provider?(url)
|
50
6
|
url.include?("youtube.com") || url.include?("youtu.be")
|
51
7
|
end
|
52
8
|
|
53
9
|
def self.get_urls_and_filenames(url, options = {})
|
54
|
-
@quality = options[:quality]
|
55
|
-
filter = options[:playlist_filter]
|
56
|
-
parser = PlaylistParser.new
|
57
|
-
return_vals = []
|
58
10
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
return_vals << grab_single_url_filename(url, options)
|
63
|
-
end
|
64
|
-
|
65
|
-
clean_return_values(return_vals)
|
66
|
-
end
|
11
|
+
@url_resolver = UrlResolver.new
|
12
|
+
@video_resolver = VideoResolver.new(Decipherer.new)
|
13
|
+
@format_picker = FormatPicker.new(options)
|
67
14
|
|
68
|
-
|
69
|
-
|
15
|
+
urls = @url_resolver.get_all_urls(url, options[:filter])
|
16
|
+
videos = get_videos(urls)
|
70
17
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
cleaned
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def self.grab_single_url_filename(url, options)
|
79
|
-
UrlGrabber.new(url, self, options).process
|
80
|
-
end
|
81
|
-
|
82
|
-
class UrlGrabber
|
83
|
-
attr_accessor :url, :options, :plugin, :quality
|
84
|
-
|
85
|
-
def initialize(url, plugin, options)
|
86
|
-
@url = url
|
87
|
-
@plugin = plugin
|
88
|
-
@options = options
|
89
|
-
@quality = options[:quality]
|
90
|
-
end
|
91
|
-
|
92
|
-
def process
|
93
|
-
grab_url_embeddable(url) || grab_url_non_embeddable(url)
|
94
|
-
end
|
95
|
-
|
96
|
-
# VEVO video: http://www.youtube.com/watch?v=A_J7kEhY9sM
|
97
|
-
# Non-VEVO video: http://www.youtube.com/watch?v=WkkC9cK8Hz0
|
98
|
-
|
99
|
-
def grab_url_embeddable(url)
|
100
|
-
video_info = get_video_info(url)
|
101
|
-
video_params = extract_video_parameters(video_info)
|
102
|
-
|
103
|
-
unless video_params[:embeddable]
|
104
|
-
Youtube.notify("VIDEO IS NOT EMBEDDABLE")
|
105
|
-
return false
|
106
|
-
end
|
107
|
-
|
108
|
-
urls_formats = extract_urls_formats(video_info)
|
109
|
-
selected_format = choose_format(urls_formats)
|
110
|
-
title = video_params[:title]
|
111
|
-
file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
|
112
|
-
|
113
|
-
{:url => urls_formats[selected_format], :name => file_name}
|
114
|
-
end
|
115
|
-
|
116
|
-
def grab_url_non_embeddable(url)
|
117
|
-
video_info = open(url).read
|
118
|
-
stream_map = video_info[/url_encoded_fmt_stream_map\" *: *\"([^\"]+)\"/,1]
|
119
|
-
|
120
|
-
# Video has been deleted!
|
121
|
-
if stream_map.nil? or !stream_map.index("been+removed").nil?
|
122
|
-
Youtube.notify("VIDEO IS REMOVED")
|
123
|
-
return false
|
124
|
-
end
|
125
|
-
|
126
|
-
urls_formats = parse_stream_map(url_decode(stream_map))
|
127
|
-
selected_format = choose_format(urls_formats)
|
128
|
-
title = video_info[/<meta name="title" content="([^"]*)">/, 1]
|
129
|
-
file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
|
130
|
-
|
131
|
-
# cleaning
|
132
|
-
clean_url = urls_formats[selected_format].gsub(/\\u0026[^&]*/, "").split(',type=video').first
|
133
|
-
{:url => clean_url, :name => file_name}
|
134
|
-
end
|
135
|
-
|
136
|
-
def get_video_info(url)
|
137
|
-
id = extract_video_id(url)
|
138
|
-
request_url = VIDEO_INFO_URL + id + VIDEO_INFO_PARMS
|
139
|
-
open(request_url).read
|
140
|
-
end
|
141
|
-
|
142
|
-
def extract_video_parameters(video_info)
|
143
|
-
video_params = CGI.parse(url_decode(video_info))
|
144
|
-
|
145
|
-
{
|
146
|
-
:title => video_params["title"].first,
|
147
|
-
:length_sec => video_params["length_seconds"].first,
|
148
|
-
:author => video_params["author"].first,
|
149
|
-
:embeddable => (video_params["status"].first != "fail")
|
150
|
-
}
|
151
|
-
end
|
152
|
-
|
153
|
-
def extract_video_id(url)
|
154
|
-
# the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
|
155
|
-
# addition: might also look like this /v/abc5-a5afe5agae6g
|
156
|
-
# alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
|
157
|
-
url = open(url).base_uri.to_s if url.include?("youtu.be")
|
158
|
-
video_id = url[/(v|embed)[=\/]([^\/\?\&]*)/, 2]
|
159
|
-
|
160
|
-
if video_id
|
161
|
-
Youtube.notify("ID FOUND: #{video_id}")
|
162
|
-
video_id
|
163
|
-
else
|
164
|
-
Youtube.download_error("No video id found.")
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
def extract_urls_formats(video_info)
|
169
|
-
stream_map = video_info[/url_encoded_fmt_stream_map=(.+?)(?:&|$)/, 1]
|
170
|
-
parse_stream_map(stream_map)
|
171
|
-
end
|
172
|
-
|
173
|
-
def choose_format(urls_formats)
|
174
|
-
available_formats = urls_formats.keys
|
175
|
-
|
176
|
-
if @quality #if the user specified a format
|
177
|
-
ext = @quality[:extension]
|
178
|
-
res = @quality[:resolution] || ""
|
179
|
-
#gets a nested array with all the formats of the same res as the user wanted
|
180
|
-
requested = VIDEO_FORMATS.select { |id, format| available_formats.include?(id) && format[:name].include?(res) }.to_a
|
181
|
-
|
182
|
-
if requested.empty?
|
183
|
-
Youtube.notify "Requested format \"#{res}:#{ext}\" not found. Downloading default format."
|
184
|
-
get_default_format(available_formats)
|
185
|
-
else
|
186
|
-
pick = requested.find { |format| format[1][:extension] == ext } # get requsted extension if possible
|
187
|
-
pick ? pick.first : get_default_format(requested.map { |req| req.first }) # else return the default format
|
188
|
-
end
|
189
|
-
else
|
190
|
-
get_default_format(available_formats)
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
def parse_stream_map(stream_map)
|
195
|
-
urls = extract_download_urls(stream_map)
|
196
|
-
formats_urls = {}
|
197
|
-
|
198
|
-
urls.each do |url|
|
199
|
-
format = url[/itag=(\d+)/, 1]
|
200
|
-
formats_urls[format] = url
|
201
|
-
end
|
202
|
-
|
203
|
-
formats_urls
|
204
|
-
end
|
205
|
-
|
206
|
-
def extract_download_urls(stream_map)
|
207
|
-
entries = stream_map.split("%2C")
|
208
|
-
decoded = entries.map { |entry| url_decode(entry) }
|
209
|
-
|
210
|
-
decoded.map do |entry|
|
211
|
-
url = entry[/url=(.*?itag=.+?)(?:itag=|;|$)/, 1]
|
212
|
-
sig = entry[/sig=(.+?)(?:&|$)/, 1]
|
213
|
-
|
214
|
-
url + "&signature=#{sig}"
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
def get_default_format(available)
|
219
|
-
DEFAULT_FORMAT_ORDER.find { |default| available.include?(default) }
|
220
|
-
end
|
221
|
-
|
222
|
-
def url_decode(text)
|
223
|
-
while text != (decoded = CGI::unescape(text)) do
|
224
|
-
text = decoded
|
225
|
-
end
|
226
|
-
text
|
18
|
+
return_value = videos.map do |video|
|
19
|
+
format = @format_picker.pick_format(video)
|
20
|
+
make_url_filname_hash(video, format)
|
227
21
|
end
|
228
22
|
|
23
|
+
return_value.empty? ? download_error("No videos could be downloaded.") : return_value
|
229
24
|
end
|
230
25
|
|
231
26
|
def self.notify(message)
|
@@ -236,83 +31,23 @@ class Youtube < PluginBase
|
|
236
31
|
raise CouldNotDownloadVideoError, message
|
237
32
|
end
|
238
33
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
def get_playlist_urls(url, filter = nil)
|
249
|
-
@filter = filter
|
250
|
-
|
251
|
-
if url.include?("view_play_list") || url.include?("playlist?list=") # if playlist URL
|
252
|
-
parse_playlist(url)
|
253
|
-
elsif username = url[/\/(?:user|channel)\/([\w\d]+)(?:\/|$)/, 1] # if user/channel URL
|
254
|
-
parse_user(username)
|
255
|
-
else # if neither return nil
|
256
|
-
nil
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
def parse_playlist(url)
|
261
|
-
#http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
|
262
|
-
#http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
|
263
|
-
#http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
|
264
|
-
|
265
|
-
playlist_ID = url[/(?:list=PL|p=)(.+?)(?:&|\/|$)/, 1]
|
266
|
-
Youtube.notify "Playlist ID: #{playlist_ID}"
|
267
|
-
feed_url = PLAYLIST_FEED % playlist_ID
|
268
|
-
url_array = get_video_urls(feed_url)
|
269
|
-
Youtube.notify "#{url_array.size} links found!"
|
270
|
-
url_array
|
271
|
-
end
|
272
|
-
|
273
|
-
def parse_user(username)
|
274
|
-
Youtube.notify "User: #{username}"
|
275
|
-
feed_url = USER_FEED % username
|
276
|
-
url_array = get_video_urls(feed_url)
|
277
|
-
Youtube.notify "#{url_array.size} links found!"
|
278
|
-
url_array
|
279
|
-
end
|
280
|
-
|
281
|
-
#get all videos and return their urls in an array
|
282
|
-
def get_video_urls(feed_url)
|
283
|
-
Youtube.notify "Retrieving videos..."
|
284
|
-
urls_titles = {}
|
285
|
-
result_feed = Nokogiri::XML(open(feed_url))
|
286
|
-
urls_titles.merge!(grab_urls_and_titles(result_feed))
|
287
|
-
|
288
|
-
#as long as the feed has a next link we follow it and add the resulting video urls
|
289
|
-
loop do
|
290
|
-
next_link = result_feed.search("//feed/link[@rel='next']").first
|
291
|
-
break if next_link.nil?
|
292
|
-
result_feed = Nokogiri::HTML(open(next_link["href"]))
|
293
|
-
urls_titles.merge!(grab_urls_and_titles(result_feed))
|
34
|
+
def self.get_videos(urls)
|
35
|
+
videos = urls.map do |url|
|
36
|
+
begin
|
37
|
+
@video_resolver.get_video(url)
|
38
|
+
rescue VideoResolver::VideoRemovedError
|
39
|
+
notify "The video #{url} has been removed."
|
40
|
+
rescue => e
|
41
|
+
notify "Error getting the video: #{e.message}"
|
294
42
|
end
|
295
|
-
|
296
|
-
filter_urls(urls_titles)
|
297
43
|
end
|
298
44
|
|
299
|
-
|
300
|
-
|
301
|
-
feed.remove_namespaces! #so that we can get to the titles easily
|
302
|
-
urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
|
303
|
-
titles = feed.search("//entry/group/title").map { |title| title.text }
|
304
|
-
Hash[urls.zip(titles)] #hash like this: url => title
|
305
|
-
end
|
45
|
+
videos.reject(&:nil?)
|
46
|
+
end
|
306
47
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
filtered = url_hash.select { |url, title| title =~ @filter }
|
312
|
-
filtered.keys
|
313
|
-
else
|
314
|
-
url_hash.keys
|
315
|
-
end
|
316
|
-
end
|
48
|
+
def self.make_url_filname_hash(video, format)
|
49
|
+
url = video.get_download_url(format.itag)
|
50
|
+
name = PluginBase.make_filename_safe(video.title) + ".#{format.extension}"
|
51
|
+
{url: url, name: name}
|
317
52
|
end
|
318
53
|
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
|
2
|
+
class Decipherer
|
3
|
+
|
4
|
+
class UnknownCipherVersionError < StandardError; end
|
5
|
+
class UnknownCipherOperationError < StandardError; end
|
6
|
+
|
7
|
+
CIPHERS = {
|
8
|
+
'vflNzKG7n' => 's3 r s2 r s1 r w67', # 30 Jan 2013, untested
|
9
|
+
'vfllMCQWM' => 's2 w46 r w27 s2 w43 s2 r', # 15 Feb 2013, untested
|
10
|
+
'vflJv8FA8' => 's1 w51 w52 r', # 12 Mar 2013, untested
|
11
|
+
'vflR_cX32' => 's2 w64 s3', # 11 Apr 2013, untested
|
12
|
+
'vflveGye9' => 'w21 w3 s1 r w44 w36 r w41 s1', # 02 May 2013, untested
|
13
|
+
'vflj7Fxxt' => 'r s3 w3 r w17 r w41 r s2', # 14 May 2013, untested
|
14
|
+
'vfltM3odl' => 'w60 s1 w49 r s1 w7 r s2 r', # 23 May 2013
|
15
|
+
'vflDG7-a-' => 'w52 r s3 w21 r s3 r', # 06 Jun 2013
|
16
|
+
'vfl39KBj1' => 'w52 r s3 w21 r s3 r', # 12 Jun 2013
|
17
|
+
'vflmOfVEX' => 'w52 r s3 w21 r s3 r', # 21 Jun 2013
|
18
|
+
'vflJwJuHJ' => 'r s3 w19 r s2', # 25 Jun 2013
|
19
|
+
'vfl_ymO4Z' => 'r s3 w19 r s2', # 26 Jun 2013
|
20
|
+
'vfl26ng3K' => 'r s2 r', # 08 Jul 2013
|
21
|
+
'vflcaqGO8' => 'w24 w53 s2 w31 w4', # 11 Jul 2013
|
22
|
+
'vflQw-fB4' => 's2 r s3 w9 s3 w43 s3 r w23', # 16 Jul 2013
|
23
|
+
'vflSAFCP9' => 'r s2 w17 w61 r s1 w7 s1', # 18 Jul 2013
|
24
|
+
'vflART1Nf' => 's3 r w63 s2 r s1', # 22 Jul 2013
|
25
|
+
'vflLC8JvQ' => 'w34 w29 w9 r w39 w24', # 25 Jul 2013
|
26
|
+
'vflm_D8eE' => 's2 r w39 w55 w49 s3 w56 w2', # 30 Jul 2013
|
27
|
+
'vflTWC9KW' => 'r s2 w65 r', # 31 Jul 2013
|
28
|
+
'vflRFcHMl' => 's3 w24 r', # 04 Aug 2013
|
29
|
+
'vflM2EmfJ' => 'w10 r s1 w45 s2 r s3 w50 r', # 06 Aug 2013
|
30
|
+
'vflz8giW0' => 's2 w18 s3', # 07 Aug 2013
|
31
|
+
'vfl_wGgYV' => 'w60 s1 r s1 w9 s3 r s3 r', # 08 Aug 2013
|
32
|
+
'vfl1HXdPb' => 'w52 r w18 r s1 w44 w51 r s1', # 12 Aug 2013
|
33
|
+
'vflkn6DAl' => 'w39 s2 w57 s2 w23 w35 s2', # 15 Aug 2013
|
34
|
+
'vfl2LOvBh' => 'w34 w19 r s1 r s3 w24 r', # 16 Aug 2013
|
35
|
+
'vfl-bxy_m' => 'w48 s3 w37 s2', # 20 Aug 2013
|
36
|
+
'vflZK4ZYR' => 'w19 w68 s1', # 21 Aug 2013
|
37
|
+
'vflh9ybst' => 'w48 s3 w37 s2', # 21 Aug 2013
|
38
|
+
'vflapUV9V' => 's2 w53 r w59 r s2 w41 s3', # 27 Aug 2013
|
39
|
+
'vflg0g8PQ' => 'w36 s3 r s2', # 28 Aug 2013
|
40
|
+
'vflHOr_nV' => 'w58 r w50 s1 r s1 r w11 s3', # 30 Aug 2013
|
41
|
+
'vfluy6kdb' => 'r w12 w32 r w34 s3 w35 w42 s2', # 05 Sep 2013
|
42
|
+
'vflkuzxcs' => 'w22 w43 s3 r s1 w43', # 10 Sep 2013
|
43
|
+
'vflGNjMhJ' => 'w43 w2 w54 r w8 s1', # 12 Sep 2013
|
44
|
+
'vfldJ8xgI' => 'w11 r w29 s1 r s3', # 17 Sep 2013
|
45
|
+
'vfl79wBKW' => 's3 r s1 r s3 r s3 w59 s2', # 19 Sep 2013
|
46
|
+
'vflg3FZfr' => 'r s3 w66 w10 w43 s2', # 24 Sep 2013
|
47
|
+
'vflUKrNpT' => 'r s2 r w63 r', # 25 Sep 2013
|
48
|
+
'vfldWnjUz' => 'r s1 w68', # 30 Sep 2013
|
49
|
+
'vflP7iCEe' => 'w7 w37 r s1', # 03 Oct 2013
|
50
|
+
'vflzVne63' => 'w59 s2 r', # 07 Oct 2013
|
51
|
+
'vflO-N-9M' => 'w9 s1 w67 r s3', # 09 Oct 2013
|
52
|
+
'vflZ4JlpT' => 's3 r s1 r w28 s1', # 11 Oct 2013
|
53
|
+
'vflDgXSDS' => 's3 r s1 r w28 s1', # 15 Oct 2013
|
54
|
+
'vflW444Sr' => 'r w9 r s1 w51 w27 r s1 r', # 17 Oct 2013
|
55
|
+
'vflK7RoTQ' => 'w44 r w36 r w45', # 21 Oct 2013
|
56
|
+
'vflKOCFq2' => 's1 r w41 r w41 s1 w15', # 23 Oct 2013
|
57
|
+
'vflcLL31E' => 's1 r w41 r w41 s1 w15', # 28 Oct 2013
|
58
|
+
'vflz9bT3N' => 's1 r w41 r w41 s1 w15', # 31 Oct 2013
|
59
|
+
'vfliZsE79' => 'r s3 w49 s3 r w58 s2 r s2', # 05 Nov 2013
|
60
|
+
'vfljOFtAt' => 'r s3 r s1 r w69 r', # 07 Nov 2013
|
61
|
+
'vflqSl9GX' => 'w32 r s2 w65 w26 w45 w24 w40 s2', # 14 Nov 2013
|
62
|
+
'vflFrKymJ' => 'w32 r s2 w65 w26 w45 w24 w40 s2', # 15 Nov 2013
|
63
|
+
'vflKz4WoM' => 'w50 w17 r w7 w65', # 19 Nov 2013
|
64
|
+
'vflhdWW8S' => 's2 w55 w10 s3 w57 r w25 w41', # 21 Nov 2013
|
65
|
+
'vfl66X2C5' => 'r s2 w34 s2 w39', # 26 Nov 2013
|
66
|
+
'vflCXG8Sm' => 'r s2 w34 s2 w39', # 02 Dec 2013
|
67
|
+
'vfl_3Uag6' => 'w3 w7 r s2 w27 s2 w42 r', # 04 Dec 2013
|
68
|
+
'vflQdXVwM' => 's1 r w66 s2 r w12', # 10 Dec 2013
|
69
|
+
'vflCtc3aO' => 's2 r w11 r s3 w28', # 12 Dec 2013
|
70
|
+
'vflCt6YZX' => 's2 r w11 r s3 w28', # 17 Dec 2013
|
71
|
+
'vflG49soT' => 'w32 r s3 r s1 r w19 w24 s3', # 18 Dec 2013
|
72
|
+
'vfl4cHApe' => 'w25 s1 r s1 w27 w21 s1 w39', # 06 Jan 2014
|
73
|
+
'vflwMrwdI' => 'w3 r w39 r w51 s1 w36 w14', # 06 Jan 2014
|
74
|
+
'vfl4AMHqP' => 'r s1 w1 r w43 r s1 r', # 09 Jan 2014
|
75
|
+
'vfln8xPyM' => 'w36 w14 s1 r s1 w54', # 10 Jan 2014
|
76
|
+
'vflVSLmnY' => 's3 w56 w10 r s2 r w28 w35', # 13 Jan 2014
|
77
|
+
'vflkLvpg7' => 'w4 s3 w53 s2', # 15 Jan 2014
|
78
|
+
'vflbxes4n' => 'w4 s3 w53 s2', # 15 Jan 2014
|
79
|
+
'vflmXMtFI' => 'w57 s3 w62 w41 s3 r w60 r', # 23 Jan 2014
|
80
|
+
'vflYDqEW1' => 'w24 s1 r s2 w31 w4 w11 r', # 24 Jan 2014
|
81
|
+
'vflapGX6Q' => 's3 w2 w59 s2 w68 r s3 r s1', # 28 Jan 2014
|
82
|
+
'vflLCYwkM' => 's3 w2 w59 s2 w68 r s3 r s1', # 29 Jan 2014
|
83
|
+
'vflcY_8N0' => 's2 w36 s1 r w18 r w19 r', # 30 Jan 2014
|
84
|
+
'vfl9qWoOL' => 'w68 w64 w28 r', # 03 Feb 2014
|
85
|
+
'vfle-mVwz' => 's3 w7 r s3 r w14 w59 s3 r', # 04 Feb 2014
|
86
|
+
'vfltdb6U3' => 'w61 w5 r s2 w69 s2 r', # 05 Feb 2014
|
87
|
+
'vflLjFx3B' => 'w40 w62 r s2 w21 s3 r w7 s3', # 10 Feb 2014
|
88
|
+
'vfliqjKfF' => 'w40 w62 r s2 w21 s3 r w7 s3', # 13 Feb 2014
|
89
|
+
'ima-vflxBu-5R' => 'w40 w62 r s2 w21 s3 r w7 s3', # 13 Feb 2014
|
90
|
+
'ima-vflrGwWV9' => 'w36 w45 r s2 r' # 20 Feb 2014
|
91
|
+
}
|
92
|
+
|
93
|
+
def decipher_with_version(cipher, cipher_version)
|
94
|
+
operations = CIPHERS[cipher_version]
|
95
|
+
raise UnknownCipherVersionError.new("Unknown cipher version: #{cipher_version}") unless operations
|
96
|
+
|
97
|
+
decipher_with_operations(cipher, operations.split)
|
98
|
+
end
|
99
|
+
|
100
|
+
def decipher_with_operations(cipher, operations)
|
101
|
+
cipher = cipher.dup
|
102
|
+
|
103
|
+
operations.each do |op|
|
104
|
+
cipher = apply_operation(cipher, op)
|
105
|
+
end
|
106
|
+
cipher
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def apply_operation(cipher, op)
|
112
|
+
op = check_operation(op)
|
113
|
+
|
114
|
+
case op[0].downcase
|
115
|
+
when "r"
|
116
|
+
cipher.reverse
|
117
|
+
when "w"
|
118
|
+
index = get_op_index(op)
|
119
|
+
swap_first_char(cipher, index)
|
120
|
+
when "s"
|
121
|
+
index = get_op_index(op)
|
122
|
+
cipher[index, cipher.length - 1] # slice from index to the end
|
123
|
+
else
|
124
|
+
raise_unknown_op_error(op)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def check_operation(op)
|
129
|
+
raise_unknown_op_error(op) if op.nil? || !op.respond_to?(:to_s)
|
130
|
+
op.to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
def swap_first_char(string, index)
|
134
|
+
temp = string[0]
|
135
|
+
string[0] = string[index]
|
136
|
+
string[index] = temp
|
137
|
+
string
|
138
|
+
end
|
139
|
+
|
140
|
+
def get_op_index(op)
|
141
|
+
index = op[/.(\d+)/, 1]
|
142
|
+
raise_unknown_op_error(op) unless index
|
143
|
+
index.to_i
|
144
|
+
end
|
145
|
+
|
146
|
+
def raise_unknown_op_error(op)
|
147
|
+
raise UnknownCipherOperationError.new("Unkown operation: #{op}")
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
|
2
|
+
class FormatPicker
|
3
|
+
|
4
|
+
Format = Struct.new(:itag, :extension, :resolution, :name)
|
5
|
+
Resolution = Struct.new(:width, :height)
|
6
|
+
|
7
|
+
# see http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
|
8
|
+
# TODO: we don't have all the formats from the wiki article here
|
9
|
+
# :u means the resolution is unknown.
|
10
|
+
FORMATS = [
|
11
|
+
Format.new("38", "mp4", Resolution.new(4096, 3027), "MP4 Highest Quality 4096x3027 (H.264, AAC)"),
|
12
|
+
Format.new("37", "mp4", Resolution.new(1920, 1080), "MP4 Highest Quality 1920x1080 (H.264, AAC)"),
|
13
|
+
Format.new("22", "mp4", Resolution.new(1280, 720), "MP4 1280x720 (H.264, AAC)"),
|
14
|
+
Format.new("46", "webm", Resolution.new(1920, 1080), "WebM 1920x1080 (VP8, Vorbis)"),
|
15
|
+
Format.new("45", "webm", Resolution.new(1280, 720), "WebM 1280x720 (VP8, Vorbis)"),
|
16
|
+
Format.new("44", "webm", Resolution.new(854, 480), "WebM 854x480 (VP8, Vorbis)"),
|
17
|
+
Format.new("43", "webm", Resolution.new(480, 360), "WebM 480x360 (VP8, Vorbis)"),
|
18
|
+
Format.new("18", "mp4", Resolution.new(640, 360), "MP4 640x360 (H.264, AAC)"),
|
19
|
+
Format.new("35", "flv", Resolution.new(854, 480), "FLV 854x480 (H.264, AAC)"),
|
20
|
+
Format.new("34", "flv", Resolution.new(640, 360), "FLV 640x360 (H.264, AAC)"),
|
21
|
+
Format.new("6", "flv", Resolution.new(640, 360), "FLV 640x360 (Soerenson H.263)"),
|
22
|
+
Format.new("5", "flv", Resolution.new(400, 240), "FLV 400x240 (Soerenson H.263)"),
|
23
|
+
Format.new("36", "3gp", Resolution.new(320, 240), "3gp Medium Quality - 320x240 (MPEG-4 Visual, AAC)"),
|
24
|
+
Format.new("17", "3gp", Resolution.new(174, 144), "3gp Medium Quality - 176x144 (MPEG-4 Visual, AAC)"),
|
25
|
+
Format.new("13", "3gp", Resolution.new(176, 144), "3gp Low Quality - 176x144 (MPEG-4 Visual, AAC)"),
|
26
|
+
Format.new("82", "mp4", Resolution.new(480, 360), "MP4 360p (H.264 AAC)"),
|
27
|
+
Format.new("83", "mp4", Resolution.new(320, 240), "MP4 240p (H.264 AAC)"),
|
28
|
+
Format.new("84", "mp4", Resolution.new(1280, 720), "MP4 720p (H.264 AAC)"),
|
29
|
+
Format.new("85", "mp4", Resolution.new(960, 520), "MP4 520p (H.264 AAC)"),
|
30
|
+
Format.new("100", "webm", Resolution.new(480, 360), "WebM 360p (VP8 Vorbis)"),
|
31
|
+
Format.new("101", "webm", Resolution.new(480, 360), "WebM 360p (VP8 Vorbis)"),
|
32
|
+
Format.new("102", "webm", Resolution.new(1280, 720), "WebM 720p (VP8 Vorbis)"),
|
33
|
+
Format.new("120", "flv", Resolution.new(1280, 720), "FLV 720p (H.264 AAC)"),
|
34
|
+
Format.new("133", "mp4", Resolution.new(320, 240), "MP4 240p (H.264)"),
|
35
|
+
Format.new("134", "mp4", Resolution.new(480, 360), "MP4 360p (H.264)"),
|
36
|
+
Format.new("135", "mp4", Resolution.new(640, 480), "MP4 480p (H.264)"),
|
37
|
+
Format.new("136", "mp4", Resolution.new(1280, 720), "MP4 720p (H.264)"),
|
38
|
+
Format.new("137", "mp4", Resolution.new(1920, 1080), "MP4 1080p (H.264)"),
|
39
|
+
Format.new("139", "mp4", Resolution.new(:u, :u), "MP4 (AAC)"),
|
40
|
+
Format.new("140", "mp4", Resolution.new(:u, :u), "MP4 (AAC"),
|
41
|
+
Format.new("141", "mp4", Resolution.new(:u, :u), "MP4 (AAC)"),
|
42
|
+
Format.new("160", "mp4", Resolution.new(:u, :u), "MP4 (H.264)"),
|
43
|
+
Format.new("171", "webm", Resolution.new(:u, :u), "WebM (Vorbis)"),
|
44
|
+
Format.new("172", "webm", Resolution.new(:u, :u), "WebM (Vorbis)")
|
45
|
+
]
|
46
|
+
|
47
|
+
DEFAULT_FORMAT_ORDER = %w[38 37 22 46 45 44 43 18 35 34 6 5 36 17 13 82 83 84 85 100 101 102 120 133 134 135 136 137 139 140 141 160 171 172]
|
48
|
+
|
49
|
+
def initialize(options)
|
50
|
+
@options = options
|
51
|
+
end
|
52
|
+
|
53
|
+
def pick_format(video)
|
54
|
+
if quality = @options[:quality]
|
55
|
+
get_quality_format(video, quality)
|
56
|
+
else
|
57
|
+
get_default_format_for_video(video)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def get_default_format_for_video(video)
|
64
|
+
available = get_available_formats_for_video(video)
|
65
|
+
get_default_format(available)
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_available_formats_for_video(video)
|
69
|
+
video.available_itags.map { |itag| get_format_by_itag(itag) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_format_by_itag(itag)
|
73
|
+
FORMATS.find { |format| format.itag == itag }
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_default_format(formats)
|
77
|
+
DEFAULT_FORMAT_ORDER.each do |itag|
|
78
|
+
default_format = formats.find { |format| format.itag == itag }
|
79
|
+
return default_format if default_format
|
80
|
+
end
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_quality_format(video, quality)
|
85
|
+
available = get_available_formats_for_video(video)
|
86
|
+
|
87
|
+
matches = available.select do |format|
|
88
|
+
matches_extension?(format, quality) && matches_resolution?(format, quality)
|
89
|
+
end
|
90
|
+
|
91
|
+
select_format(video, matches)
|
92
|
+
end
|
93
|
+
|
94
|
+
def matches_extension?(format, quality)
|
95
|
+
return false if quality[:extension] && quality[:extension] != format.extension
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def matches_resolution?(format, quality)
|
100
|
+
return false if quality[:width] && quality[:width] != format.resolution.width
|
101
|
+
return false if quality[:height] && quality[:height] != format.resolution.height
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
def select_format(video, formats)
|
106
|
+
case formats.length
|
107
|
+
when 0
|
108
|
+
Youtube.notify "Requested format not found. Downloading default format."
|
109
|
+
get_default_format_for_video(video)
|
110
|
+
when 1
|
111
|
+
formats.first
|
112
|
+
else
|
113
|
+
get_default_format(matches_resolution)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class UrlResolver
|
2
|
+
|
3
|
+
PLAYLIST_FEED = "http://gdata.youtube.com/feeds/api/playlists/%s?&max-results=50&v=2"
|
4
|
+
USER_FEED = "http://gdata.youtube.com/feeds/api/users/%s/uploads?&max-results=50&v=2"
|
5
|
+
|
6
|
+
def get_all_urls(url, filter = nil)
|
7
|
+
@filter = filter
|
8
|
+
|
9
|
+
if url.include?("view_play_list") || url.include?("playlist?list=") # if playlist URL
|
10
|
+
parse_playlist(url)
|
11
|
+
elsif username = url[/\/(?:user|channel)\/([\w\d]+)(?:\/|$)/, 1] # if user/channel URL
|
12
|
+
parse_user(username)
|
13
|
+
else # if neither return nil
|
14
|
+
[url]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parse_playlist(url)
|
21
|
+
#http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
|
22
|
+
#http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
|
23
|
+
#http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
|
24
|
+
|
25
|
+
playlist_ID = url[/(?:list=PL|p=)(.+?)(?:&|\/|$)/, 1]
|
26
|
+
Youtube.notify "Playlist ID: #{playlist_ID}"
|
27
|
+
feed_url = PLAYLIST_FEED % playlist_ID
|
28
|
+
url_array = get_video_urls(feed_url)
|
29
|
+
Youtube.notify "#{url_array.size} links found!"
|
30
|
+
url_array
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_user(username)
|
34
|
+
Youtube.notify "User: #{username}"
|
35
|
+
feed_url = USER_FEED % username
|
36
|
+
url_array = get_video_urls(feed_url)
|
37
|
+
Youtube.notify "#{url_array.size} links found!"
|
38
|
+
url_array
|
39
|
+
end
|
40
|
+
|
41
|
+
#get all videos and return their urls in an array
|
42
|
+
def get_video_urls(feed_url)
|
43
|
+
Youtube.notify "Retrieving videos..."
|
44
|
+
urls_titles = {}
|
45
|
+
result_feed = Nokogiri::XML(open(feed_url))
|
46
|
+
urls_titles.merge!(grab_urls_and_titles(result_feed))
|
47
|
+
|
48
|
+
#as long as the feed has a next link we follow it and add the resulting video urls
|
49
|
+
loop do
|
50
|
+
next_link = result_feed.search("//feed/link[@rel='next']").first
|
51
|
+
break if next_link.nil?
|
52
|
+
result_feed = Nokogiri::HTML(open(next_link["href"]))
|
53
|
+
urls_titles.merge!(grab_urls_and_titles(result_feed))
|
54
|
+
end
|
55
|
+
|
56
|
+
filter_urls(urls_titles)
|
57
|
+
end
|
58
|
+
|
59
|
+
#extract all video urls and their titles from a feed and return in a hash
|
60
|
+
def grab_urls_and_titles(feed)
|
61
|
+
feed.remove_namespaces! #so that we can get to the titles easily
|
62
|
+
urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
|
63
|
+
titles = feed.search("//entry/group/title").map { |title| title.text }
|
64
|
+
Hash[urls.zip(titles)] #hash like this: url => title
|
65
|
+
end
|
66
|
+
|
67
|
+
#returns only the urls that match the --filter argument regex (if present)
|
68
|
+
def filter_urls(url_hash)
|
69
|
+
if @filter
|
70
|
+
Youtube.notify "Using filter: #{@filter}"
|
71
|
+
filtered = url_hash.select { |url, title| title =~ @filter }
|
72
|
+
filtered.keys
|
73
|
+
else
|
74
|
+
url_hash.keys
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
|
2
|
+
class VideoResolver
|
3
|
+
|
4
|
+
class VideoRemovedError < StandardError; end
|
5
|
+
|
6
|
+
CORRECT_SIGNATURE_LENGTH = 81
|
7
|
+
SIGNATURE_URL_PARAMETER = "signature"
|
8
|
+
|
9
|
+
def initialize(decipherer)
|
10
|
+
@decipherer = decipherer
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_video(url)
|
14
|
+
@json = load_json(url)
|
15
|
+
Video.new(get_title, parse_stream_map(get_stream_map))
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def load_json(url)
|
21
|
+
html = open(url).read
|
22
|
+
json_data = html[/ytplayer\.config\s*=\s*(\{.+?\});/m, 1]
|
23
|
+
MultiJson.load(json_data)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_stream_map
|
27
|
+
stream_map = @json["args"]["url_encoded_fmt_stream_map"]
|
28
|
+
raise VideoRemovedError.new if stream_map.nil? || stream_map.include?("been+removed")
|
29
|
+
stream_map
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_html5player_version
|
33
|
+
@json["assets"]["js"][/html5player-(.+?)\.js/, 1]
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_title
|
37
|
+
@json["args"]["title"]
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Returns a an array of hashes in the following format:
|
42
|
+
# [
|
43
|
+
# {format: format_id, url: download_url},
|
44
|
+
# {format: format_id, url: download_url}
|
45
|
+
# ...
|
46
|
+
# ]
|
47
|
+
#
|
48
|
+
def parse_stream_map(stream_map)
|
49
|
+
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
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_stream_map_entry(entry)
|
57
|
+
# Note: CGI.parse puts each value in an array.
|
58
|
+
params = CGI.parse((entry))
|
59
|
+
|
60
|
+
{
|
61
|
+
itag: params["itag"].first,
|
62
|
+
sig: fetch_signature(params),
|
63
|
+
url: url_decode(params["url"].first)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# 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
|
69
|
+
# applied and the the video can be downloaded directly.
|
70
|
+
def fetch_signature(params)
|
71
|
+
sig = params.fetch("sig", nil) || params.fetch("s", nil)
|
72
|
+
sig && sig.first
|
73
|
+
end
|
74
|
+
|
75
|
+
def url_decode(text)
|
76
|
+
while text != (decoded = CGI::unescape(text)) do
|
77
|
+
text = decoded
|
78
|
+
end
|
79
|
+
text
|
80
|
+
end
|
81
|
+
|
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
|
87
|
+
|
88
|
+
def get_deciphered_sig(sig)
|
89
|
+
return sig if sig.length == CORRECT_SIGNATURE_LENGTH
|
90
|
+
#crequire 'pry'; binding.pry; exit
|
91
|
+
@decipherer.decipher_with_version(sig, get_html5player_version)
|
92
|
+
end
|
93
|
+
|
94
|
+
class Video
|
95
|
+
attr_reader :title
|
96
|
+
|
97
|
+
def initialize(title, itags_urls)
|
98
|
+
@title = title
|
99
|
+
@itags_urls = itags_urls
|
100
|
+
end
|
101
|
+
|
102
|
+
def available_itags
|
103
|
+
@itags_urls.map { |iu| iu[:itag] }
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_download_url(itag)
|
107
|
+
itag_url = @itags_urls.find { |iu| iu[:itag] == itag }
|
108
|
+
itag_url[:url] if itag_url
|
109
|
+
end
|
110
|
+
end
|
111
|
+
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.
|
4
|
+
version: "0.96"
|
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-02-
|
12
|
+
date: 2014-02-25 00:00:00 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mime-types
|
@@ -95,6 +95,10 @@ files:
|
|
95
95
|
- plugins/soundcloud.rb
|
96
96
|
- plugins/veoh.rb
|
97
97
|
- plugins/vimeo.rb
|
98
|
+
- plugins/youtube/decipherer.rb
|
99
|
+
- plugins/youtube/format_picker.rb
|
100
|
+
- plugins/youtube/url_resolver.rb
|
101
|
+
- plugins/youtube/video_resolver.rb
|
98
102
|
- plugins/youtube.rb
|
99
103
|
- Gemfile
|
100
104
|
- Gemfile.lock
|
@@ -123,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
127
|
requirements: []
|
124
128
|
|
125
129
|
rubyforge_project: viddl-rb
|
126
|
-
rubygems_version: 2.
|
130
|
+
rubygems_version: 2.1.11
|
127
131
|
signing_key:
|
128
132
|
specification_version: 4
|
129
133
|
summary: An extendable commandline video downloader for flash video sites.
|