viddl-rb 0.8 → 0.61
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/CHANGELOG.txt +14 -0
- data/Gemfile.lock +7 -29
- data/README.md +34 -122
- data/Rakefile +8 -22
- data/bin/viddl-rb +117 -45
- data/helper/download-helper.rb +67 -94
- data/helper/plugin-helper.rb +10 -60
- data/plugins/blip.rb +2 -3
- data/plugins/metacafe.rb +54 -53
- data/plugins/soundcloud.rb +15 -19
- data/plugins/veoh.rb +41 -40
- data/plugins/vimeo.rb +23 -30
- data/plugins/youtube.rb +138 -266
- metadata +29 -50
- data/TODO.txt +0 -3
- data/bin/helper/downloader.rb +0 -20
- data/bin/helper/driver.rb +0 -47
- data/bin/helper/parameter-parser.rb +0 -109
- data/helper/audio-helper.rb +0 -48
- data/helper/utility-helper.rb +0 -45
- data/lib/viddl-rb.rb +0 -103
- data/plugins/dailymotion.rb +0 -44
data/helper/plugin-helper.rb
CHANGED
@@ -1,60 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
class
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
attr_accessor :io
|
12
|
-
attr_reader :registered_plugins
|
13
|
-
end
|
14
|
-
|
15
|
-
#all calls to #puts, #print and #p from any plugin instance will be redirected to this object
|
16
|
-
@io = $stdout
|
17
|
-
@registered_plugins = []
|
18
|
-
|
19
|
-
#if you inherit from this class, the child gets added to the "registered plugins" array
|
20
|
-
def self.inherited(child)
|
21
|
-
PluginBase.registered_plugins << child
|
22
|
-
end
|
23
|
-
|
24
|
-
#takes a string a returns a new string that is file name safe
|
25
|
-
#deletes \"' and replaces anything else that is not a digit or letter with _
|
26
|
-
def self.make_filename_safe(string)
|
27
|
-
string.delete("\"'").gsub(/[^\d\w]/, '_')
|
28
|
-
end
|
29
|
-
|
30
|
-
#the following methods redirects the Kernel printing methods (except #p) to the
|
31
|
-
#PluginBase IO object. this is because sometimes we want plugins to
|
32
|
-
#write to something else than $stdout
|
33
|
-
|
34
|
-
def self.puts(*objects)
|
35
|
-
PluginBase.io.puts(*objects)
|
36
|
-
nil
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.print(*objects)
|
40
|
-
PluginBase.io.print(*objects)
|
41
|
-
nil
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.putc(int)
|
45
|
-
PluginBase.io.putc(int)
|
46
|
-
nil
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.printf(string, *objects)
|
50
|
-
if string.is_a?(IO) || string.is_a?(StringIO)
|
51
|
-
super(string, *objects) # so we don't redirect the printf that prints to a separate IO object
|
52
|
-
else
|
53
|
-
PluginBase.io.printf(string, *objects)
|
54
|
-
end
|
55
|
-
nil
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
end
|
60
|
-
|
1
|
+
class PluginBase
|
2
|
+
#some static stuff
|
3
|
+
class << self; attr_reader :registered_plugins end
|
4
|
+
@registered_plugins = []
|
5
|
+
|
6
|
+
#if you inherit from this class, the child gets added to the "registered plugins" array
|
7
|
+
def self.inherited(child)
|
8
|
+
PluginBase.registered_plugins << child
|
9
|
+
end
|
10
|
+
end
|
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)
|
@@ -6,12 +5,12 @@ class Blip < PluginBase
|
|
6
5
|
end
|
7
6
|
|
8
7
|
# return the url for original video file and title
|
9
|
-
def self.get_urls_and_filenames(url
|
8
|
+
def self.get_urls_and_filenames(url)
|
10
9
|
id = self.to_id(url)
|
11
10
|
xml_url = "http://blip.tv/rss/#{id}"
|
12
11
|
doc = Nokogiri::XML(open(xml_url))
|
13
12
|
user = doc.at("//channel/item/blip:user").inner_text
|
14
|
-
title =
|
13
|
+
title = doc.at("//channel/item/title").inner_text.gsub(" ", "_")
|
15
14
|
download_url = doc.at("//channel/item/media:group/media:content").attributes["url"].value
|
16
15
|
extention = download_url.split(".").last
|
17
16
|
file_name = "#{id}-#{user}-#{title}.#{extention}"
|
data/plugins/metacafe.rb
CHANGED
@@ -3,58 +3,59 @@
|
|
3
3
|
# Vidoes that have URLs that look like this: http://www.metacafe.com/watch/cb-q78rA_lp9s1_9EJsqKJ5BdIHdDNuHa1l/ cannot be downloaded.
|
4
4
|
|
5
5
|
class Metacafe < PluginBase
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
6
|
+
BASE_FILE_URL = "http://v.mccont.com/ItemFiles/%5BFrom%20www.metacafe.com%5D%20"
|
7
|
+
API_BASE = "http://www.metacafe.com/api/"
|
8
|
+
|
9
|
+
#this will be called by the main app to check whether this plugin is responsible for the url passed
|
10
|
+
def self.matches_provider?(url)
|
11
|
+
url.include?("metacafe.com")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get_urls_and_filenames(url)
|
15
|
+
video_id = get_video_id(url)
|
16
|
+
info_url = API_BASE + "item/#{video_id}" #use the API to get the full video url
|
17
|
+
info_doc = Nokogiri::XML(open(info_url))
|
18
|
+
|
19
|
+
video_swf_url = get_video_swf_url(info_doc, video_id)
|
20
|
+
|
21
|
+
#by getting the video swf url we get a http redirect url with all info needed
|
22
|
+
http_response = Net::HTTP.get_response(URI(video_swf_url))
|
23
|
+
redirect_url = http_response['location']
|
24
|
+
|
25
|
+
file_info = get_file_info(redirect_url, video_id)
|
26
|
+
key_string = get_file_key(redirect_url)
|
27
|
+
file_url_with_key = file_info[:file_url] + "?__gda__=#{key_string}"
|
28
|
+
escaped_url = CGI::escape(file_url_with_key)
|
29
|
+
|
30
|
+
[{:url => escaped_url, :name => get_video_name(video_swf_url) + file_info[:extension]}]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.get_video_id(url)
|
34
|
+
id = url[/watch\/(\d+)/, 1]
|
35
|
+
unless id
|
36
|
+
puts "ERROR: Can only download videos that has the ID in the URL."
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
id
|
40
|
+
end
|
24
41
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
#$1 = file name part 1, $2 = file name part 2, $3 = file extension
|
47
|
-
def self.get_file_info(redirect_url, video_id)
|
48
|
-
redirect_url =~ /mediaURL.+?metacafe\.com%.+?%\d+\.(\d+)\.(\d+)(\.[\d\w]+)/
|
49
|
-
{:file_url => "#{BASE_FILE_URL}#{video_id}\.#{$1}\.#{$2}#{$3}", :extension => $3}
|
50
|
-
end
|
51
|
-
|
52
|
-
def self.get_file_key(redirect_url)
|
53
|
-
redirect_url[/key.+?value":"([\w\d]+)"/, 1]
|
54
|
-
end
|
55
|
-
|
56
|
-
def self.get_video_name(url)
|
57
|
-
name = url[/fplayer\/\d+\/([\d\w]+)\.swf/, 1]
|
58
|
-
PluginBase.make_filename_safe(name)
|
59
|
-
end
|
42
|
+
def self.get_video_swf_url(info_doc, video_id)
|
43
|
+
video_url = info_doc.xpath("//rss/channel/item/link").text
|
44
|
+
video_url.sub!("watch", "fplayer")
|
45
|
+
video_url.sub!(/\/\z/, ".swf") # remove last '/' and add .swf in it's place
|
46
|
+
end
|
47
|
+
|
48
|
+
#$1 = file name part 1, $2 = file name part 2, $3 = file extension
|
49
|
+
def self.get_file_info(redirect_url, video_id)
|
50
|
+
redirect_url =~ /mediaURL.+?metacafe\.com%.+?%\d+\.(\d+)\.(\d+)(\.[\d\w]+)/
|
51
|
+
{:file_url => "#{BASE_FILE_URL}#{video_id}\.#{$1}\.#{$2}#{$3}", :extension => $3}
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.get_file_key(redirect_url)
|
55
|
+
redirect_url[/key.+?\%22([\w\d]+?)\%22/, 1]
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.get_video_name(url)
|
59
|
+
url[/fplayer\/\d+\/([\d\w]+)\.swf/, 1]
|
60
|
+
end
|
60
61
|
end
|
data/plugins/soundcloud.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'open-uri'
|
2
1
|
class Soundcloud < 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)
|
@@ -6,8 +5,8 @@ class Soundcloud < PluginBase
|
|
6
5
|
end
|
7
6
|
|
8
7
|
# return the url for original video file and title
|
9
|
-
def self.get_urls_and_filenames(url
|
10
|
-
doc = Nokogiri::
|
8
|
+
def self.get_urls_and_filenames(url)
|
9
|
+
doc = Nokogiri::XML(open(url))
|
11
10
|
download_filename = doc.at("#main-content-inner img[class=waveform]").attributes["src"].value.to_s.match(/\.com\/(.+)\_/)[1]
|
12
11
|
download_url = "http://media.soundcloud.com/stream/#{download_filename}"
|
13
12
|
file_name = transliterate("#{doc.at('//h1/em').text.chomp}") + ".mp3"
|
@@ -16,27 +15,24 @@ class Soundcloud < PluginBase
|
|
16
15
|
end
|
17
16
|
|
18
17
|
def self.transliterate(str)
|
19
|
-
|
18
|
+
# Based on permalink_fu by Rick Olsen
|
20
19
|
|
21
|
-
|
22
|
-
|
20
|
+
# Downcase string
|
21
|
+
str.downcase!
|
23
22
|
|
24
|
-
|
25
|
-
|
23
|
+
# Remove apostrophes so isn't changes to isnt
|
24
|
+
str.gsub!(/'/, '')
|
26
25
|
|
27
|
-
|
28
|
-
|
26
|
+
# Replace any non-letter or non-number character with a space
|
27
|
+
str.gsub!(/[^A-Za-z0-9]+/, ' ')
|
29
28
|
|
30
|
-
|
31
|
-
|
29
|
+
# Remove spaces from beginning and end of string
|
30
|
+
str.strip!
|
32
31
|
|
33
|
-
|
34
|
-
|
32
|
+
# Replace groups of spaces with single hyphen
|
33
|
+
str.gsub!(/\ +/, '-')
|
35
34
|
|
36
|
-
|
37
|
-
|
35
|
+
str
|
36
|
+
end
|
38
37
|
|
39
|
-
def self.get_http_url(url)
|
40
|
-
url.sub(/https?:\/\//, "http:\/\/")
|
41
|
-
end
|
42
38
|
end
|
data/plugins/veoh.rb
CHANGED
@@ -1,45 +1,46 @@
|
|
1
1
|
class Veoh < PluginBase
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
2
|
+
VEOH_API_BASE = "http://www.veoh.com/api/"
|
3
|
+
PREFERRED_FORMATS = [:mp4, :flash] # mp4 is preferred because it enables downloading full videos and not just previews
|
4
|
+
|
5
|
+
#this will be called by the main app to check whether this plugin is responsible for the url passed
|
6
|
+
def self.matches_provider?(url)
|
7
|
+
url.include?("veoh.com")
|
8
|
+
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def self.get_urls_and_filenames(url)
|
11
|
+
veoh_id = url[/\/watch\/([\w\d]+)/, 1]
|
12
|
+
info_url = "#{VEOH_API_BASE}findByPermalink?permalink=#{veoh_id}"
|
13
|
+
info_doc = Nokogiri::XML(open(info_url))
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
download_url = get_download_url(info_doc)
|
16
|
+
file_name = get_file_name(info_doc, download_url)
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
18
|
+
[{:url => download_url, :name => file_name}]
|
19
|
+
end
|
20
|
+
|
21
|
+
#returns the first valid download url string, in order of the prefered formats, that is found for the video
|
22
|
+
def self.get_download_url(info_doc)
|
23
|
+
PREFERRED_FORMATS.each do |format|
|
24
|
+
a = get_attribute(format)
|
25
|
+
download_attr = info_doc.xpath('//rsp/videoList/video').first.attributes[a]
|
26
|
+
return(download_attr.content) unless download_attr.nil? || download_attr.content.empty?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#the file name string is a combination of the video name and the extension
|
31
|
+
def self.get_file_name(info_doc, download_url)
|
32
|
+
name = info_doc.xpath('//rsp/videoList/video').first.attributes['title'].content
|
33
|
+
name.gsub!(" ", "_") # replace spaces with underscores
|
34
|
+
extension = download_url[/\/[\w\d]+(\.[\w\d]+)\?ct/, 1]
|
35
|
+
name + extension
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
38
|
+
def self.get_attribute(format)
|
39
|
+
case format
|
40
|
+
when :mp4
|
41
|
+
"ipodUrl"
|
42
|
+
when :flash
|
43
|
+
"previewUrl"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/plugins/vimeo.rb
CHANGED
@@ -1,32 +1,25 @@
|
|
1
1
|
class Vimeo < PluginBase
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
2
|
+
#this will be called by the main app to check whether this plugin is responsible for the url passed
|
3
|
+
def self.matches_provider?(url)
|
4
|
+
url.include?("vimeo.com")
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.get_urls_and_filenames(url)
|
8
|
+
#the vimeo ID consists of 7 decimal numbers in the URL
|
9
|
+
vimeo_id = url[/\d{7,8}/]
|
10
|
+
doc = Nokogiri::XML(open("http://www.vimeo.com/moogaloop/load/clip:#{vimeo_id}"))
|
11
|
+
title = doc.at("//video/caption").inner_text
|
12
|
+
puts "[VIMEO] Title: #{title}"
|
13
|
+
request_signature = doc.at("//request_signature").inner_text
|
14
|
+
request_signature_expires = doc.at("//request_signature_expires").inner_text
|
15
|
+
|
10
16
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
#the timestamp and sig info is in the embedded player javascript in the video page
|
21
|
-
timestamp = page_html[/"timestamp":(\d+),/, 1]
|
22
|
-
signature = page_html[/"signature":"([\d\w]+)",/, 1]
|
23
|
-
|
24
|
-
redirect_url = "http://player.vimeo.com/play_redirect?clip_id=#{vimeo_id}&sig=#{signature}&time=#{timestamp}&quality=hd,sd&codecs=H264,VP8,VP6"
|
25
|
-
|
26
|
-
#the download url is the value of the location (redirect) header
|
27
|
-
download_url = agent.get(redirect_url).header["location"]
|
28
|
-
file_name = PluginBase.make_filename_safe(title) + ".mp4"
|
29
|
-
|
30
|
-
[{:url => download_url, :name => file_name}]
|
31
|
-
end
|
32
|
-
end
|
17
|
+
puts "[VIMEO] Request Signature: #{request_signature} expires: #{request_signature_expires}"
|
18
|
+
|
19
|
+
download_url = "http://www.vimeo.com/moogaloop/play/clip:#{vimeo_id}/#{request_signature}/#{request_signature_expires}/?q=hd"
|
20
|
+
#todo: put the filename cleaning stuff into a seperate helper
|
21
|
+
file_name = title.delete("\"'").gsub(/[^0-9A-Za-z]/, '_') + ".flv"
|
22
|
+
puts "downloading to " + file_name
|
23
|
+
[{:url => download_url, :name => file_name}]
|
24
|
+
end
|
25
|
+
end
|
data/plugins/youtube.rb
CHANGED
@@ -1,271 +1,143 @@
|
|
1
|
-
|
2
1
|
class Youtube < PluginBase
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
urls_formats = extract_urls_formats(video_info)
|
68
|
-
selected_format = choose_format(urls_formats)
|
69
|
-
title = video_params[:title]
|
70
|
-
file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
|
71
|
-
|
72
|
-
{:url => urls_formats[selected_format], :name => file_name}
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.grab_url_non_embeddable(url)
|
76
|
-
video_info = open(url).read
|
77
|
-
stream_map = video_info[/url_encoded_fmt_stream_map\" *: *\"([^\"]+)\"/,1]
|
78
|
-
urls_formats = parse_stream_map(url_decode(stream_map))
|
79
|
-
selected_format = choose_format(urls_formats)
|
80
|
-
title = video_info[/<meta name="title" content="([^"]*)">/, 1]
|
81
|
-
file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
|
82
|
-
|
83
|
-
# cleaning
|
84
|
-
clean_url = urls_formats[selected_format].gsub(/\\u0026[^&]*/, "")
|
85
|
-
|
86
|
-
{:url => clean_url, :name => file_name}
|
87
|
-
end
|
88
|
-
|
89
|
-
def self.get_video_info(url)
|
90
|
-
id = extract_video_id(url)
|
91
|
-
request_url = VIDEO_INFO_URL + id + VIDEO_INFO_PARMS
|
92
|
-
open(request_url).read
|
93
|
-
end
|
94
|
-
|
95
|
-
def self.extract_video_id(url)
|
96
|
-
# the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
|
97
|
-
# addition: might also look like this /v/abc5-a5afe5agae6g
|
98
|
-
# alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
|
99
|
-
url = open(url).base_uri.to_s if url.include?("youtu.be")
|
100
|
-
video_id = url[/(v|embed)[=\/]([^\/\?\&]*)/, 2]
|
101
|
-
|
102
|
-
if video_id
|
103
|
-
notify("ID FOUND: #{video_id}")
|
104
|
-
video_id
|
105
|
-
else
|
106
|
-
download_error("No video id found.")
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def self.extract_video_parameters(video_info)
|
111
|
-
decoded = url_decode(video_info)
|
112
|
-
|
113
|
-
{:title => decoded[/title=(.+?)(?:&|$)/, 1],
|
114
|
-
:length_sec => decoded[/length_seconds=(.+?)(?:&|$)/, 1],
|
115
|
-
:author => decoded[/author=(.+?)(?:&|$)/, 1],
|
116
|
-
:embeddable => !decoded.include?("status=fail")}
|
117
|
-
end
|
118
|
-
|
119
|
-
def self.extract_urls_formats(video_info)
|
120
|
-
stream_map = video_info[/url_encoded_fmt_stream_map=(.+?)(?:&|$)/, 1]
|
121
|
-
parse_stream_map(stream_map)
|
122
|
-
end
|
123
|
-
|
124
|
-
def self.parse_stream_map(stream_map)
|
125
|
-
urls = extract_download_urls(stream_map)
|
126
|
-
formats_urls = {}
|
127
|
-
|
128
|
-
urls.each do |url|
|
129
|
-
format = url[/itag=(\d+)/, 1]
|
130
|
-
formats_urls[format] = url
|
131
|
-
end
|
132
|
-
|
133
|
-
formats_urls
|
134
|
-
end
|
135
|
-
|
136
|
-
def self.extract_download_urls(stream_map)
|
137
|
-
entries = stream_map.split("%2C")
|
138
|
-
decoded = entries.map { |entry| url_decode(entry) }
|
139
|
-
|
140
|
-
decoded.map do |entry|
|
141
|
-
url = entry[/url=(.*?itag=.+?)(?:itag=|;|$)/, 1]
|
142
|
-
sig = entry[/sig=(.+?)(?:&|$)/, 1]
|
143
|
-
|
144
|
-
url + "&signature=#{sig}"
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def self.choose_format(urls_formats)
|
149
|
-
available_formats = urls_formats.keys
|
150
|
-
|
151
|
-
if @quality #if the user specified a format
|
152
|
-
ext = @quality[:extension]
|
153
|
-
res = @quality[:resolution]
|
154
|
-
#gets a nested array with all the formats of the same res as the user wanted
|
155
|
-
requested = VIDEO_FORMATS.select { |id, format| format[:name].include?(res) }.to_a
|
156
|
-
|
157
|
-
if requested.empty?
|
158
|
-
notify "Requested format \"#{res}:#{ext}\" not found. Downloading default format."
|
159
|
-
get_default_format(available_formats)
|
2
|
+
#this will be called by the main app to check whether this plugin is responsible for the url passed
|
3
|
+
def self.matches_provider?(url)
|
4
|
+
url.include?("youtube.com") || url.include?("youtu.be")
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.parse_playlist(url)
|
8
|
+
#http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
|
9
|
+
#http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
|
10
|
+
#http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
|
11
|
+
|
12
|
+
playlist_ID = url[/p=(\w{16})&?/,1]
|
13
|
+
puts "[YOUTUBE] Playlist ID: #{playlist_ID}"
|
14
|
+
url_array = Array.new
|
15
|
+
video_info = Nokogiri::HTML(open("http://gdata.youtube.com/feeds/api/playlists/#{playlist_ID}?v=2"))
|
16
|
+
video_info.search("//content").each do |video|
|
17
|
+
url_array << video["url"] if video["url"].include?("http://www.youtube.com/v/") #filters out rtsp links
|
18
|
+
end
|
19
|
+
|
20
|
+
puts "[YOUTUBE] #{url_array.size} links found!"
|
21
|
+
url_array
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def self.get_urls_and_filenames(url)
|
26
|
+
return_values = []
|
27
|
+
if url.include?("view_play_list")
|
28
|
+
puts "[YOUTUBE] playlist found! analyzing..."
|
29
|
+
files = self.parse_playlist(url)
|
30
|
+
puts "[YOUTUBE] Starting playlist download"
|
31
|
+
files.each do |file|
|
32
|
+
puts "[YOUTUBE] Downloading next movie on the playlist (#{file})"
|
33
|
+
return_values << self.grab_single_url_filename(url)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
return_values << self.grab_single_url_filename(url)
|
37
|
+
end
|
38
|
+
return_values
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.grab_single_url_filename(url)
|
42
|
+
#the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
|
43
|
+
#addition: might also look like this /v/abc5-a5afe5agae6g
|
44
|
+
# alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
|
45
|
+
# First get the redirect
|
46
|
+
if url.include?("youtu.be")
|
47
|
+
url = open(url).base_uri.to_s
|
48
|
+
end
|
49
|
+
video_id = url[/(v|embed)[\/=]([^\/\?\&]*)/,2]
|
50
|
+
if video_id.nil?
|
51
|
+
puts "no video id found."
|
52
|
+
exit
|
53
|
+
else
|
54
|
+
puts "[YOUTUBE] ID FOUND: #{video_id}"
|
55
|
+
end
|
56
|
+
#let's get some infos about the video. data is urlencoded
|
57
|
+
yt_url = "http://www.youtube.com/get_video_info?video_id=#{video_id}"
|
58
|
+
video_info = open(yt_url).read
|
59
|
+
#converting the huge infostring into a hash. simply by splitting it at the & and then splitting it into key and value arround the =
|
60
|
+
#[...]blabla=blubb&narf=poit&marc=awesome[...]
|
61
|
+
video_info_hash = Hash[*video_info.split("&").collect { |v|
|
62
|
+
key, encoded_value = v.split("=")
|
63
|
+
if encoded_value.to_s.empty?
|
64
|
+
value = ""
|
160
65
|
else
|
161
|
-
|
162
|
-
|
66
|
+
#decode until everything is "normal"
|
67
|
+
while (encoded_value != CGI::unescape(encoded_value)) do
|
68
|
+
#"decoding"
|
69
|
+
encoded_value = CGI::unescape(encoded_value)
|
70
|
+
end
|
71
|
+
value = encoded_value
|
163
72
|
end
|
164
|
-
else
|
165
|
-
get_default_format(available_formats)
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def self.get_default_format(available)
|
170
|
-
DEFAULT_FORMAT_ORDER.find { |default| available.include?(default) }
|
171
|
-
end
|
172
|
-
|
173
|
-
def self.url_decode(text)
|
174
|
-
while text != (decoded = CGI::unescape(text)) do
|
175
|
-
text = decoded
|
176
|
-
end
|
177
|
-
text
|
178
|
-
end
|
179
|
-
|
180
|
-
def self.notify(message)
|
181
|
-
puts "[YOUTUBE] #{message}"
|
182
|
-
end
|
183
|
-
|
184
|
-
def self.download_error(message)
|
185
|
-
raise CouldNotDownloadVideoError, message
|
186
|
-
end
|
187
|
-
|
188
|
-
#
|
189
|
-
# class PlaylistParser
|
190
|
-
#_____________________
|
191
|
-
|
192
|
-
class PlaylistParser
|
193
|
-
|
194
|
-
PLAYLIST_FEED = "http://gdata.youtube.com/feeds/api/playlists/%s?&max-results=50&v=2"
|
195
|
-
USER_FEED = "http://gdata.youtube.com/feeds/api/users/%s/uploads?&max-results=50&v=2"
|
196
|
-
|
197
|
-
def get_playlist_urls(url, filter = nil)
|
198
|
-
@filter = filter
|
199
73
|
|
200
|
-
if
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
74
|
+
if key =~ /_map/
|
75
|
+
orig_value = value
|
76
|
+
value = value.split(",")
|
77
|
+
if key == "url_encoded_fmt_stream_map"
|
78
|
+
url_array = orig_value.split("url=").map{|url_string| url_string.chomp(",")}
|
79
|
+
result_hash = {}
|
80
|
+
url_array.each do |url|
|
81
|
+
next if url.to_s.empty?
|
82
|
+
format_id = url.match(/\&itag=(\d+)/)[1]
|
83
|
+
result_hash[format_id] = url
|
84
|
+
end
|
85
|
+
value = result_hash
|
86
|
+
elsif key == "fmt_map"
|
87
|
+
value = Hash[*value.collect{ |v|
|
88
|
+
k2, *v2 = v.split("/")
|
89
|
+
[k2, v2]
|
90
|
+
}.flatten(1)]
|
91
|
+
elsif key == "fmt_url_map" || key == "fmt_stream_map"
|
92
|
+
Hash[*value.collect { |v| v.split("|")}.flatten]
|
93
|
+
end
|
206
94
|
end
|
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
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
#returns only the urls that match the --filter argument regex (if present)
|
257
|
-
def filter_urls(url_hash)
|
258
|
-
if @filter
|
259
|
-
notify "Using filter: #{@filter}"
|
260
|
-
filtered = url_hash.select { |url, title| title =~ @filter }
|
261
|
-
filtered.keys
|
262
|
-
else
|
263
|
-
url_hash.keys
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def notify(message)
|
268
|
-
Youtube.notify(message)
|
269
|
-
end
|
270
|
-
end
|
271
|
-
end
|
95
|
+
[key, value]
|
96
|
+
}.flatten]
|
97
|
+
|
98
|
+
if video_info_hash["status"] == "fail"
|
99
|
+
puts "Error: embedding disabled, no video info found"
|
100
|
+
exit
|
101
|
+
end
|
102
|
+
|
103
|
+
title = video_info_hash["title"]
|
104
|
+
length_s = video_info_hash["length_seconds"]
|
105
|
+
token = video_info_hash["token"]
|
106
|
+
|
107
|
+
|
108
|
+
#for the formats, see: http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
|
109
|
+
fmt_list = video_info_hash["fmt_list"].split(",")
|
110
|
+
available_formats = fmt_list.map{|format| format.split("/").first}
|
111
|
+
|
112
|
+
format_ext = {}
|
113
|
+
format_ext["38"] = {:extension => "mp4", :name => "MP4 Highest Quality 4096x3027 (H.264, AAC)"}
|
114
|
+
format_ext["37"] = {:extension => "mp4", :name => "MP4 Highest Quality 1920x1080 (H.264, AAC)"}
|
115
|
+
format_ext["22"] = {:extension => "mp4", :name => "MP4 1280x720 (H.264, AAC)"}
|
116
|
+
format_ext["45"] = {:extension => "webm", :name => "WebM 1280x720 (VP8, Vorbis)"}
|
117
|
+
format_ext["44"] = {:extension => "webm", :name => "WebM 854x480 (VP8, Vorbis)"}
|
118
|
+
format_ext["18"] = {:extension => "mp4", :name => "MP4 640x360 (H.264, AAC)"}
|
119
|
+
format_ext["35"] = {:extension => "flv", :name => "FLV 854x480 (H.264, AAC)"}
|
120
|
+
format_ext["34"] = {:extension => "flv", :name => "FLV 640x360 (H.264, AAC)"}
|
121
|
+
format_ext["5"] = {:extension => "flv", :name => "FLV 400x240 (Soerenson H.263)"}
|
122
|
+
format_ext["17"] = {:extension => "3gp", :name => "3gp"}
|
123
|
+
|
124
|
+
#since 1.8 doesn't do ordered hashes
|
125
|
+
prefered_order = ["38","37","22","45","44","18","35","34","5","17"]
|
126
|
+
|
127
|
+
selected_format = prefered_order.select{|possible_format| available_formats.include?(possible_format)}.first
|
128
|
+
|
129
|
+
puts "[YOUTUBE] Title: #{title}"
|
130
|
+
puts "[YOUTUBE] Length: #{length_s} s"
|
131
|
+
puts "[YOUTUBE] t-parameter: #{token}"
|
132
|
+
#best quality seems always to be firsts
|
133
|
+
puts "[YOUTUBE] formats available: #{available_formats.inspect} (downloading format #{selected_format} -> #{format_ext[selected_format][:name]})"
|
134
|
+
|
135
|
+
#video_info_hash.keys.sort.each{|key| puts "#{key} : #{video_info_hash[key]}" }
|
136
|
+
download_url = video_info_hash["url_encoded_fmt_stream_map"][selected_format]
|
137
|
+
#if download url ends with a ';' followed by a codec string remove that part because it stops URI.parse from working
|
138
|
+
download_url = $1 if download_url =~ /(.*?);\scodecs=/
|
139
|
+
file_name = title.delete("\"'").gsub(/[^0-9A-Za-z]/, '_') + "." + format_ext[selected_format][:extension]
|
140
|
+
puts "downloading to " + file_name
|
141
|
+
{:url => download_url, :name => file_name}
|
142
|
+
end
|
143
|
+
end
|