viddl-rb 0.76 → 0.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/plugins/youtube.rb +254 -237
- metadata +5 -7
- data/Gemfile.lock +0 -47
data/plugins/youtube.rb
CHANGED
@@ -1,237 +1,254 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
"
|
9
|
-
"
|
10
|
-
"
|
11
|
-
"45" => {:extension => "webm", :name => "WebM 1280x720 (VP8, Vorbis)"},
|
12
|
-
"44" => {:extension => "webm", :name => "WebM 854x480 (VP8, Vorbis)"},
|
13
|
-
"
|
14
|
-
"
|
15
|
-
"
|
16
|
-
"
|
17
|
-
"
|
18
|
-
}
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
41
|
-
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
#
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
end
|
1
|
+
|
2
|
+
class Youtube < PluginBase
|
3
|
+
|
4
|
+
# see http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
|
5
|
+
# TODO: we don't have all the formats from the wiki article here
|
6
|
+
VIDEO_FORMATS = {
|
7
|
+
"38" => {:extension => "mp4", :name => "MP4 Highest Quality 4096x3027 (H.264, AAC)"},
|
8
|
+
"37" => {:extension => "mp4", :name => "MP4 Highest Quality 1920x1080 (H.264, AAC)"},
|
9
|
+
"22" => {:extension => "mp4", :name => "MP4 1280x720 (H.264, AAC)"},
|
10
|
+
"46" => {:extension => "webm", :name => "WebM 1920x1080 (VP8, Vorbis)"},
|
11
|
+
"45" => {:extension => "webm", :name => "WebM 1280x720 (VP8, Vorbis)"},
|
12
|
+
"44" => {:extension => "webm", :name => "WebM 854x480 (VP8, Vorbis)"},
|
13
|
+
"43" => {:extension => "webm", :name => "WebM 480×360 (VP8, Vorbis)"},
|
14
|
+
"18" => {:extension => "mp4", :name => "MP4 640x360 (H.264, AAC)"},
|
15
|
+
"35" => {:extension => "flv", :name => "FLV 854x480 (H.264, AAC)"},
|
16
|
+
"34" => {:extension => "flv", :name => "FLV 640x360 (H.264, AAC)"},
|
17
|
+
"5" => {:extension => "flv", :name => "FLV 400x240 (Soerenson H.263)"},
|
18
|
+
"17" => {:extension => "3gp", :name => "3gp"}
|
19
|
+
}
|
20
|
+
|
21
|
+
DEFAULT_FORMAT_ORDER = %w[38 37 22 45 44 18 35 34 5 7]
|
22
|
+
VIDEO_INFO_URL = "http://www.youtube.com/get_video_info?video_id="
|
23
|
+
VIDEO_INFO_PARMS = "&ps=default&eurl=&gl=US&hl=en"
|
24
|
+
|
25
|
+
# this will be called by the main app to check whether this plugin is responsible for the url passed
|
26
|
+
def self.matches_provider?(url)
|
27
|
+
url.include?("youtube.com") || url.include?("youtu.be")
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.get_urls_and_filenames(url, options = {})
|
31
|
+
@quality = options[:quality]
|
32
|
+
filter = options[:playlist_filter]
|
33
|
+
parser = PlaylistParser.new
|
34
|
+
return_vals = []
|
35
|
+
|
36
|
+
if playlist_urls = parser.get_playlist_urls(url, filter)
|
37
|
+
playlist_urls.each { |url| return_vals << grab_single_url_filename(url) }
|
38
|
+
else
|
39
|
+
return_vals << grab_single_url_filename(url)
|
40
|
+
end
|
41
|
+
|
42
|
+
clean_return_values(return_vals)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.clean_return_values(return_values)
|
46
|
+
cleaned = return_values.reject { |val| val == :no_embed }
|
47
|
+
|
48
|
+
if cleaned.empty?
|
49
|
+
download_error("No videos could be downloaded.")
|
50
|
+
else
|
51
|
+
cleaned
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.grab_single_url_filename(url)
|
56
|
+
video_info = get_video_info(url)
|
57
|
+
video_params = extract_video_parameters(video_info)
|
58
|
+
|
59
|
+
if video_params[:embeddable]
|
60
|
+
urls_formats = extract_urls_formats(video_info)
|
61
|
+
selected_format = choose_format(urls_formats)
|
62
|
+
title = video_params[:title]
|
63
|
+
file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
|
64
|
+
|
65
|
+
{:url => urls_formats[selected_format], :name => file_name}
|
66
|
+
else
|
67
|
+
notify "Video is not embeddable and can't be downloaded."
|
68
|
+
:no_embed
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.get_video_info(url)
|
73
|
+
id = extract_video_id(url)
|
74
|
+
request_url = VIDEO_INFO_URL + id + VIDEO_INFO_PARMS
|
75
|
+
open(request_url).read
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.extract_video_id(url)
|
79
|
+
# the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
|
80
|
+
# addition: might also look like this /v/abc5-a5afe5agae6g
|
81
|
+
# alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
|
82
|
+
url = open(url).base_uri.to_s if url.include?("youtu.be")
|
83
|
+
video_id = url[/(v|embed)[=\/]([^\/\?\&]*)/, 2]
|
84
|
+
|
85
|
+
if video_id
|
86
|
+
notify("ID FOUND: #{video_id}")
|
87
|
+
video_id
|
88
|
+
else
|
89
|
+
download_error("No video id found.")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.extract_video_parameters(video_info)
|
94
|
+
decoded = url_decode(video_info)
|
95
|
+
|
96
|
+
{:title => decoded[/title=(.+?)(?:&|$)/, 1],
|
97
|
+
:length_sec => decoded[/length_seconds=(.+?)(?:&|$)/, 1],
|
98
|
+
:author => decoded[/author=(.+?)(?:&|$)/, 1],
|
99
|
+
:embeddable => !decoded.include?("status=fail")}
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.extract_urls_formats(video_info)
|
103
|
+
stream_map = video_info[/url_encoded_fmt_stream_map=(.+?)(?:&|$)/, 1]
|
104
|
+
parse_stream_map(stream_map)
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.parse_stream_map(stream_map)
|
108
|
+
urls = extract_download_urls(stream_map)
|
109
|
+
formats_urls = {}
|
110
|
+
|
111
|
+
urls.each do |url|
|
112
|
+
format = url[/itag=(\d+)/, 1]
|
113
|
+
formats_urls[format] = url
|
114
|
+
end
|
115
|
+
|
116
|
+
formats_urls
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.extract_download_urls(stream_map)
|
120
|
+
entries = stream_map.split("%2C")
|
121
|
+
decoded = entries.map { |entry| url_decode(entry) }
|
122
|
+
|
123
|
+
decoded.map do |entry|
|
124
|
+
url = entry[/url=(.*?itag=.+?)(?:itag=|;|$)/, 1]
|
125
|
+
sig = entry[/sig=(.+?)(?:&|$)/, 1]
|
126
|
+
|
127
|
+
url + "&signature=#{sig}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.choose_format(urls_formats)
|
132
|
+
available_formats = urls_formats.keys
|
133
|
+
|
134
|
+
if @quality #if the user specified a format
|
135
|
+
ext = @quality[:extension]
|
136
|
+
res = @quality[:resolution]
|
137
|
+
#gets a nested array with all the formats of the same res as the user wanted
|
138
|
+
requested = VIDEO_FORMATS.select { |id, format| format[:name].include?(res) }.to_a
|
139
|
+
|
140
|
+
if requested.empty?
|
141
|
+
notify "Requested format \"#{res}:#{ext}\" not found. Downloading default format."
|
142
|
+
get_default_format(available_formats)
|
143
|
+
else
|
144
|
+
pick = requested.find { |format| format[1][:extension] == ext } # get requsted extension if possible
|
145
|
+
pick ? pick.first : get_default_format(requested.map { |req| req.first }) # else return the default format
|
146
|
+
end
|
147
|
+
else
|
148
|
+
get_default_format(available_formats)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.get_default_format(available)
|
153
|
+
DEFAULT_FORMAT_ORDER.find { |default| available.include?(default) }
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.url_decode(text)
|
157
|
+
while text != (decoded = CGI::unescape(text)) do
|
158
|
+
text = decoded
|
159
|
+
end
|
160
|
+
text
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.notify(message)
|
164
|
+
puts "[YOUTUBE] #{message}"
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.download_error(message)
|
168
|
+
raise CouldNotDownloadVideoError, message
|
169
|
+
end
|
170
|
+
|
171
|
+
#
|
172
|
+
# class PlaylistParser
|
173
|
+
#_____________________
|
174
|
+
|
175
|
+
class PlaylistParser
|
176
|
+
|
177
|
+
PLAYLIST_FEED = "http://gdata.youtube.com/feeds/api/playlists/%s?&max-results=50&v=2"
|
178
|
+
USER_FEED = "http://gdata.youtube.com/feeds/api/users/%s/uploads?&max-results=50&v=2"
|
179
|
+
|
180
|
+
def get_playlist_urls(url, filter = nil)
|
181
|
+
@filter = filter
|
182
|
+
|
183
|
+
if url.include?("view_play_list") || url.include?("playlist?list=") # if playlist URL
|
184
|
+
parse_playlist(url)
|
185
|
+
elsif username = url[/\/user\/([\w\d]+)(?:\/|$)/, 1] # if user URL
|
186
|
+
parse_user(username)
|
187
|
+
else # if neither return nil
|
188
|
+
nil
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def parse_playlist(url)
|
193
|
+
#http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
|
194
|
+
#http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
|
195
|
+
#http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
|
196
|
+
|
197
|
+
playlist_ID = url[/(?:list=PL|p=)(.+?)(?:&|\/|$)/, 1]
|
198
|
+
notify "Playlist ID: #{playlist_ID}"
|
199
|
+
feed_url = PLAYLIST_FEED % playlist_ID
|
200
|
+
url_array = get_video_urls(feed_url)
|
201
|
+
notify "#{url_array.size} links found!"
|
202
|
+
url_array
|
203
|
+
end
|
204
|
+
|
205
|
+
def parse_user(username)
|
206
|
+
notify "User: #{username}"
|
207
|
+
feed_url = USER_FEED % username
|
208
|
+
url_array = get_video_urls(feed_url)
|
209
|
+
notify "#{url_array.size} links found!"
|
210
|
+
url_array
|
211
|
+
end
|
212
|
+
|
213
|
+
#get all videos and return their urls in an array
|
214
|
+
def get_video_urls(feed_url)
|
215
|
+
notify "Retrieving videos..."
|
216
|
+
urls_titles = {}
|
217
|
+
result_feed = Nokogiri::XML(open(feed_url))
|
218
|
+
urls_titles.merge!(grab_urls_and_titles(result_feed))
|
219
|
+
|
220
|
+
#as long as the feed has a next link we follow it and add the resulting video urls
|
221
|
+
loop do
|
222
|
+
next_link = result_feed.search("//feed/link[@rel='next']").first
|
223
|
+
break if next_link.nil?
|
224
|
+
result_feed = Nokogiri::HTML(open(next_link["href"]))
|
225
|
+
urls_titles.merge!(grab_urls_and_titles(result_feed))
|
226
|
+
end
|
227
|
+
|
228
|
+
filter_urls(urls_titles)
|
229
|
+
end
|
230
|
+
|
231
|
+
#extract all video urls and their titles from a feed and return in a hash
|
232
|
+
def grab_urls_and_titles(feed)
|
233
|
+
feed.remove_namespaces! #so that we can get to the titles easily
|
234
|
+
urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
|
235
|
+
titles = feed.search("//entry/group/title").map { |title| title.text }
|
236
|
+
Hash[urls.zip(titles)] #hash like this: url => title
|
237
|
+
end
|
238
|
+
|
239
|
+
#returns only the urls that match the --filter argument regex (if present)
|
240
|
+
def filter_urls(url_hash)
|
241
|
+
if @filter
|
242
|
+
notify "Using filter: #{@filter}"
|
243
|
+
filtered = url_hash.select { |url, title| title =~ @filter }
|
244
|
+
filtered.keys
|
245
|
+
else
|
246
|
+
url_hash.keys
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def notify(message)
|
251
|
+
Youtube.notify(message)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
metadata
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: viddl-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 145
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: "0.
|
8
|
+
- 77
|
9
|
+
version: "0.77"
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Marc Seeger
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2013-
|
17
|
+
date: 2013-02-24 00:00:00 Z
|
18
18
|
dependencies:
|
19
19
|
- !ruby/object:Gem::Dependency
|
20
20
|
name: nokogiri
|
@@ -126,7 +126,6 @@ files:
|
|
126
126
|
- plugins/vimeo.rb
|
127
127
|
- plugins/youtube.rb
|
128
128
|
- Gemfile
|
129
|
-
- Gemfile.lock
|
130
129
|
- Rakefile
|
131
130
|
- README.md
|
132
131
|
- TODO.txt
|
@@ -161,10 +160,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
161
160
|
requirements: []
|
162
161
|
|
163
162
|
rubyforge_project: viddl-rb
|
164
|
-
rubygems_version: 1.8.
|
163
|
+
rubygems_version: 1.8.15
|
165
164
|
signing_key:
|
166
165
|
specification_version: 3
|
167
166
|
summary: An extendable commandline video downloader for flash video sites.
|
168
167
|
test_files: []
|
169
168
|
|
170
|
-
has_rdoc: false
|
data/Gemfile.lock
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
viddl-rb (0.75)
|
5
|
-
mechanize
|
6
|
-
nokogiri
|
7
|
-
progressbar
|
8
|
-
rest-client
|
9
|
-
|
10
|
-
GEM
|
11
|
-
remote: http://rubygems.org/
|
12
|
-
specs:
|
13
|
-
domain_name (0.5.4)
|
14
|
-
unf (~> 0.0.3)
|
15
|
-
mechanize (2.5.1)
|
16
|
-
domain_name (~> 0.5, >= 0.5.1)
|
17
|
-
mime-types (~> 1.17, >= 1.17.2)
|
18
|
-
net-http-digest_auth (~> 1.1, >= 1.1.1)
|
19
|
-
net-http-persistent (~> 2.5, >= 2.5.2)
|
20
|
-
nokogiri (~> 1.4)
|
21
|
-
ntlm-http (~> 0.1, >= 0.1.1)
|
22
|
-
webrobots (~> 0.0, >= 0.0.9)
|
23
|
-
mime-types (1.19)
|
24
|
-
minitest (4.1.0)
|
25
|
-
net-http-digest_auth (1.2.1)
|
26
|
-
net-http-persistent (2.8)
|
27
|
-
nokogiri (1.5.5)
|
28
|
-
nokogiri (1.5.5-java)
|
29
|
-
ntlm-http (0.1.1)
|
30
|
-
progressbar (0.11.0)
|
31
|
-
rake (0.9.2.2)
|
32
|
-
rest-client (1.6.7)
|
33
|
-
mime-types (>= 1.16)
|
34
|
-
unf (0.0.5)
|
35
|
-
unf_ext
|
36
|
-
unf (0.0.5-java)
|
37
|
-
unf_ext (0.0.5)
|
38
|
-
webrobots (0.0.13)
|
39
|
-
|
40
|
-
PLATFORMS
|
41
|
-
java
|
42
|
-
ruby
|
43
|
-
|
44
|
-
DEPENDENCIES
|
45
|
-
minitest
|
46
|
-
rake
|
47
|
-
viddl-rb!
|