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.
@@ -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