viddl-rb 0.8 → 0.61

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,60 +1,10 @@
1
- module ViddlRb
2
-
3
- class PluginBase
4
-
5
- #this exception is raised by the plugins when it was not
6
- #possible to donwload the video for some reason.
7
- class CouldNotDownloadVideoError < StandardError; end
8
-
9
- #some static stuff
10
- class << self
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, options = {})
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 = PluginBase.make_filename_safe(doc.at("//channel/item/title").inner_text)
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
- 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, options = {})
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 = CGI::unescape(http_response['location'])
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
- 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
-
29
- [{:url => file_url_with_key, :name => get_video_name(video_swf_url) + file_info[:extension]}]
30
- end
31
-
32
- def self.get_video_id(url)
33
- id = url[/watch\/(\d+)/, 1]
34
- unless id
35
- raise CouldNotDownloadVideoError, "Can only download videos that has the ID in the URL."
36
- end
37
- id
38
- end
39
-
40
- def self.get_video_swf_url(info_doc, video_id)
41
- video_url = info_doc.xpath("//rss/channel/item/link").text
42
- video_url.sub!("watch", "fplayer")
43
- video_url.sub!(/\/\z/, ".swf") # remove last '/' and add .swf in it's place
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
@@ -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, options = {})
10
- doc = Nokogiri::HTML(open(get_http_url(url)))
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
- # Based on permalink_fu by Rick Olsen
18
+ # Based on permalink_fu by Rick Olsen
20
19
 
21
- # Downcase string
22
- str.downcase!
20
+ # Downcase string
21
+ str.downcase!
23
22
 
24
- # Remove apostrophes so isn't changes to isnt
25
- str.gsub!(/'/, '')
23
+ # Remove apostrophes so isn't changes to isnt
24
+ str.gsub!(/'/, '')
26
25
 
27
- # Replace any non-letter or non-number character with a space
28
- str.gsub!(/[^A-Za-z0-9]+/, ' ')
26
+ # Replace any non-letter or non-number character with a space
27
+ str.gsub!(/[^A-Za-z0-9]+/, ' ')
29
28
 
30
- # Remove spaces from beginning and end of string
31
- str.strip!
29
+ # Remove spaces from beginning and end of string
30
+ str.strip!
32
31
 
33
- # Replace groups of spaces with single hyphen
34
- str.gsub!(/\ +/, '-')
32
+ # Replace groups of spaces with single hyphen
33
+ str.gsub!(/\ +/, '-')
35
34
 
36
- str
37
- end
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
- 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
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
- def self.get_urls_and_filenames(url, options = {})
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))
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
- download_url = get_download_url(info_doc)
16
- file_name = get_file_name(info_doc, download_url)
15
+ download_url = get_download_url(info_doc)
16
+ file_name = get_file_name(info_doc, download_url)
17
17
 
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
- extension = download_url[/\/[\w\d]+(\.[\w\d]+)\?ct/, 1]
34
- PluginBase.make_filename_safe(name) + extension
35
- end
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
- def self.get_attribute(format)
38
- case format
39
- when :mp4
40
- "ipodUrl"
41
- when :flash
42
- "previewUrl"
43
- end
44
- end
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
- #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, options = {})
8
- #the vimeo ID consists of 7 decimal numbers in the URL
9
- vimeo_id = url[/\d{7,8}/]
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
- agent = Mechanize.new #use Mechanize for the automatic cookie handeling
12
- agent.redirect_ok = false #don't follow redirects so we do not download the video when we get it's url
13
-
14
- video_page = agent.get("http://vimeo.com/#{vimeo_id}")
15
- page_html = video_page.root.inner_html
16
- doc = Nokogiri::HTML(page_html)
17
- title = doc.at('meta[property="og:title"]').attributes['content'].value
18
- puts "[VIMEO] Title: #{title.inspect}"
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
- # 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 46 45 44 43 18 35 34 5 17]
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
- grab_url_embeddable(url) || grab_url_non_embeddable(url)
57
- end
58
-
59
- def self.grab_url_embeddable(url)
60
- video_info = get_video_info(url)
61
- video_params = extract_video_parameters(video_info)
62
- unless video_params[:embeddable]
63
- notify("VIDEO IS NOT EMBEDDABLE")
64
- return false
65
- end
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
- pick = requested.find { |format| format[1][:extension] == ext } # get requsted extension if possible
162
- pick ? pick.first : get_default_format(requested.map { |req| req.first }) # else return the default format
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 url.include?("view_play_list") || url.include?("playlist?list=") # if playlist URL
201
- parse_playlist(url)
202
- elsif username = url[/\/user\/([\w\d]+)(?:\/|$)/, 1] # if user URL
203
- parse_user(username)
204
- else # if neither return nil
205
- nil
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
- end
208
-
209
- def parse_playlist(url)
210
- #http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
211
- #http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
212
- #http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
213
-
214
- playlist_ID = url[/(?:list=PL|p=)(.+?)(?:&|\/|$)/, 1]
215
- notify "Playlist ID: #{playlist_ID}"
216
- feed_url = PLAYLIST_FEED % playlist_ID
217
- url_array = get_video_urls(feed_url)
218
- notify "#{url_array.size} links found!"
219
- url_array
220
- end
221
-
222
- def parse_user(username)
223
- notify "User: #{username}"
224
- feed_url = USER_FEED % username
225
- url_array = get_video_urls(feed_url)
226
- notify "#{url_array.size} links found!"
227
- url_array
228
- end
229
-
230
- #get all videos and return their urls in an array
231
- def get_video_urls(feed_url)
232
- notify "Retrieving videos..."
233
- urls_titles = {}
234
- result_feed = Nokogiri::XML(open(feed_url))
235
- urls_titles.merge!(grab_urls_and_titles(result_feed))
236
-
237
- #as long as the feed has a next link we follow it and add the resulting video urls
238
- loop do
239
- next_link = result_feed.search("//feed/link[@rel='next']").first
240
- break if next_link.nil?
241
- result_feed = Nokogiri::HTML(open(next_link["href"]))
242
- urls_titles.merge!(grab_urls_and_titles(result_feed))
243
- end
244
-
245
- filter_urls(urls_titles)
246
- end
247
-
248
- #extract all video urls and their titles from a feed and return in a hash
249
- def grab_urls_and_titles(feed)
250
- feed.remove_namespaces! #so that we can get to the titles easily
251
- urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
252
- titles = feed.search("//entry/group/title").map { |title| title.text }
253
- Hash[urls.zip(titles)] #hash like this: url => title
254
- end
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