tmm1-youtube-g 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,14 @@
1
+ * Implement video update and video delete in Upload
2
+ * Refactor the uploader slightly
3
+ * Use Builder to generate XML packets
4
+ * Use a faster Camping-based URL escaper (also avoid cgi.rb)
5
+ * Removed the logger nightmare, use YouTubeG.logger for everything whatever that may be
6
+ * Use streams for file uploads instead of in-memory strings
7
+
8
+ == 0.5.0 / 2009-01-07
9
+
10
+ * Fixed bug in user favorites (thanks Pius Uzamere)
11
+
1
12
  == 0.4.9.9 / 2008-09-01
2
13
 
3
14
  * Add Geodata information (thanks Jose Galisteo)
@@ -4,8 +4,8 @@ README.txt
4
4
  Rakefile
5
5
  TODO.txt
6
6
  lib/youtube_g.rb
7
+ lib/youtube_g/chain_io.rb
7
8
  lib/youtube_g/client.rb
8
- lib/youtube_g/logger.rb
9
9
  lib/youtube_g/model/author.rb
10
10
  lib/youtube_g/model/category.rb
11
11
  lib/youtube_g/model/contact.rb
@@ -23,6 +23,9 @@ lib/youtube_g/request/user_search.rb
23
23
  lib/youtube_g/request/video_search.rb
24
24
  lib/youtube_g/request/video_upload.rb
25
25
  lib/youtube_g/response/video_search.rb
26
+ lib/youtube_g/version.rb
27
+ test/helper.rb
28
+ test/test_chain_io.rb
26
29
  test/test_client.rb
27
30
  test/test_video.rb
28
31
  test/test_video_search.rb
data/README.txt CHANGED
@@ -1,9 +1,6 @@
1
- youtube-g
2
- by Shane Vitarana and Walter Korman
1
+ = youtube-g
3
2
 
4
- Rubyforge: http://rubyforge.org/projects/youtube-g/
5
- RDoc: http://youtube-g.rubyforge.org/
6
- Google Group: http://groups.google.com/group/ruby-youtube-library
3
+ http://youtube-g.rubyforge.org/
7
4
 
8
5
  == DESCRIPTION:
9
6
 
@@ -17,6 +14,15 @@ More detail on the underlying source Google-provided API is available at:
17
14
 
18
15
  http://code.google.com/apis/youtube/overview.html
19
16
 
17
+ == AUTHORS
18
+
19
+ Shane Vitarana and Walter Korman
20
+
21
+ == WHERE TO GET HELP
22
+
23
+ http://rubyforge.org/projects/youtube-g
24
+ http://groups.google.com/group/ruby-youtube-library
25
+
20
26
  == FEATURES/PROBLEMS:
21
27
 
22
28
  * Aims to be in parity with Google's YouTube GData API. Core functionality
@@ -49,10 +55,17 @@ Advanced queries (with boolean operators OR (either), AND (include), NOT (exclud
49
55
 
50
56
  client.videos_by(:categories => { :either => [:news, :sports], :exclude => [:comedy] }, :tags => { :include => ['football'], :exclude => ['soccer'] })
51
57
 
58
+ == LOGGING
59
+
60
+ YouTubeG passes all logs through the logger variable on the class itself. In Rails context, assign the Rails logger to that variable to collect the messages
61
+ (don't forget to set the level to debug):
62
+
63
+ YouTubeG.logger = RAILS_DEFAULT_LOGGER
64
+ RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
52
65
 
53
66
  == REQUIREMENTS:
54
67
 
55
- * None
68
+ * builder gem
56
69
 
57
70
  == INSTALL:
58
71
 
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require 'lib/youtube_g/version'
4
+
5
+ Hoe.new('youtube-g', YouTubeG::VERSION) do |p|
6
+ p.rubyforge_name = 'youtube-g'
7
+ p.author = ["Shane Vitarana", "Walter Korman", "Aman Gupta", "Filip H.F. Slagter", "msp"]
8
+ p.email = 'shanev@gmail.com'
9
+ p.summary = 'Ruby client for the YouTube GData API'
10
+ p.url = 'http://rubyforge.org/projects/youtube-g/'
11
+ p.extra_deps << 'builder'
12
+ p.remote_rdoc_dir = ''
13
+ end
14
+
15
+ desc 'Tag release'
16
+ task :tag do
17
+ svn_root = 'svn+ssh://drummr77@rubyforge.org/var/svn/youtube-g'
18
+ sh %(svn cp #{svn_root}/trunk #{svn_root}/tags/release-#{YouTubeG::VERSION} -m "Tag YouTubeG release #{YouTubeG::VERSION}")
19
+ end
20
+
21
+ desc 'Load the library in an IRB session'
22
+ task :console do
23
+ sh %(irb -r lib/youtube_g.rb)
24
+ end
@@ -3,27 +3,65 @@ require 'open-uri'
3
3
  require 'net/https'
4
4
  require 'digest/md5'
5
5
  require 'rexml/document'
6
- require 'cgi'
6
+ require 'builder'
7
7
 
8
- require File.dirname(__FILE__) + '/youtube_g/client'
9
- require File.dirname(__FILE__) + '/youtube_g/record'
10
- require File.dirname(__FILE__) + '/youtube_g/parser'
11
- require File.dirname(__FILE__) + '/youtube_g/model/author'
12
- require File.dirname(__FILE__) + '/youtube_g/model/category'
13
- require File.dirname(__FILE__) + '/youtube_g/model/contact'
14
- require File.dirname(__FILE__) + '/youtube_g/model/content'
15
- require File.dirname(__FILE__) + '/youtube_g/model/playlist'
16
- require File.dirname(__FILE__) + '/youtube_g/model/rating'
17
- require File.dirname(__FILE__) + '/youtube_g/model/thumbnail'
18
- require File.dirname(__FILE__) + '/youtube_g/model/user'
19
- require File.dirname(__FILE__) + '/youtube_g/model/video'
20
- require File.dirname(__FILE__) + '/youtube_g/request/base_search'
21
- require File.dirname(__FILE__) + '/youtube_g/request/user_search'
22
- require File.dirname(__FILE__) + '/youtube_g/request/standard_search'
23
- require File.dirname(__FILE__) + '/youtube_g/request/video_upload'
24
- require File.dirname(__FILE__) + '/youtube_g/request/video_search'
25
- require File.dirname(__FILE__) + '/youtube_g/response/video_search'
8
+ class YouTubeG
9
+
10
+ # Base error class for the extension
11
+ class Error < RuntimeError
12
+ end
13
+
14
+ # URL-escape a string. Stolen from Camping (wonder how many Ruby libs in the wild can say the same)
15
+ def self.esc(s) #:nodoc:
16
+ s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.unpack('H2'*$&.size)*'%').upcase}.tr(' ', '+')
17
+ end
18
+
19
+ # Set the logger for the library
20
+ def self.logger=(any_logger)
21
+ @logger = any_logger
22
+ end
26
23
 
27
- class YouTubeG #:nodoc:
28
- VERSION = '0.5.0'
24
+ # Get the logger for the library (by default will log to STDOUT). TODO: this is where we grab the Rails logger too
25
+ def self.logger
26
+ @logger ||= create_default_logger
27
+ end
28
+
29
+ # Gets mixed into the classes to provide the logger method
30
+ module Logging #:nodoc:
31
+
32
+ # Return the base logger set for the library
33
+ def logger
34
+ YouTubeG.logger
35
+ end
36
+ end
37
+
38
+ private
39
+ def self.create_default_logger
40
+ logger = Logger.new(STDOUT)
41
+ logger.level = Logger::DEBUG
42
+ logger
43
+ end
29
44
  end
45
+
46
+ %w(
47
+ version
48
+ client
49
+ record
50
+ parser
51
+ model/author
52
+ model/category
53
+ model/contact
54
+ model/content
55
+ model/playlist
56
+ model/rating
57
+ model/thumbnail
58
+ model/user
59
+ model/video
60
+ request/base_search
61
+ request/user_search
62
+ request/standard_search
63
+ request/video_upload
64
+ request/video_search
65
+ response/video_search
66
+ chain_io
67
+ ).each{|m| require File.dirname(__FILE__) + '/youtube_g/' + m }
@@ -0,0 +1,71 @@
1
+ require 'delegate'
2
+ #:stopdoc:
3
+
4
+ # Stream wrapper that reads IOs in succession. Can be fed to Net::HTTP as post body stream. We use it internally to stream file content
5
+ # instead of reading whole video files into memory. Strings passed to the constructor will be wrapped in StringIOs. By default it will auto-close
6
+ # file handles when they have been read completely to prevent our uploader from leaking file handles
7
+ #
8
+ # chain = ChainIO.new(File.open(__FILE__), File.open('/etc/passwd'), "abcd")
9
+ class YouTubeG::ChainIO
10
+ attr_accessor :autoclose
11
+
12
+ def initialize(*any_ios)
13
+ @autoclose = true
14
+ @chain = any_ios.flatten.map{|e| e.respond_to?(:read) ? e : StringIO.new(e.to_s) }
15
+ end
16
+
17
+ def read(buffer_size = 1024)
18
+ # Read off the first element in the stack
19
+ current_io = @chain.shift
20
+ return false if !current_io
21
+
22
+ buf = current_io.read(buffer_size)
23
+ if !buf && @chain.empty? # End of streams
24
+ release_handle(current_io) if @autoclose
25
+ false
26
+ elsif !buf # This IO is depleted, but next one is available
27
+ release_handle(current_io) if @autoclose
28
+ read(buffer_size)
29
+ elsif buf.length < buffer_size # This IO is depleted, but we were asked for more
30
+ release_handle(current_io) if @autoclose
31
+ buf + (read(buffer_size - buf.length) || '') # and recurse
32
+ else # just return the buffer
33
+ @chain.unshift(current_io) # put the current back
34
+ buf
35
+ end
36
+ end
37
+
38
+ # Predict the length of all embedded IOs. Will automatically send file size.
39
+ def expected_length
40
+ @chain.inject(0) do | len, io |
41
+ if io.respond_to?(:length)
42
+ len + (io.length - io.pos)
43
+ elsif io.is_a?(File)
44
+ len + File.size(io.path) - io.pos
45
+ else
46
+ raise "Cannot predict length of #{io.inspect}"
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+ def release_handle(io)
53
+ io.close if io.respond_to?(:close)
54
+ end
55
+ end
56
+
57
+ # Net::HTTP only can send chunks of 1024 bytes. This is very inefficient, so we have a spare IO that will send more when asked for 1024.
58
+ # We use delegation because the read call is recursive.
59
+ class YouTubeG::GreedyChainIO < DelegateClass(YouTubeG::ChainIO)
60
+ BIG_CHUNK = 512 * 1024 # 500 kb
61
+
62
+ def initialize(*with_ios)
63
+ __setobj__(YouTubeG::ChainIO.new(with_ios))
64
+ end
65
+
66
+ def read(any_buffer_size)
67
+ __getobj__.read(BIG_CHUNK)
68
+ end
69
+ end
70
+
71
+ #:startdoc:
@@ -1,9 +1,9 @@
1
1
  class YouTubeG
2
2
  class Client
3
- attr_accessor :logger
3
+ include YouTubeG::Logging
4
4
 
5
- def initialize(logger=false)
6
- @logger = Logger.new(STDOUT) if logger
5
+ # Previously this was a logger instance but we now do it globally
6
+ def initialize(legacy_debug_flag = nil)
7
7
  end
8
8
 
9
9
  # Retrieves an array of standard feed, custom query, or user videos.
@@ -53,7 +53,7 @@ class YouTubeG
53
53
  request = YouTubeG::Request::StandardSearch.new(params, request_params)
54
54
  end
55
55
 
56
- logger.debug "Submitting request [url=#{request.url}]." if logger
56
+ logger.debug "Submitting request [url=#{request.url}]."
57
57
  parser = YouTubeG::Parser::VideosFeedParser.new(request.url)
58
58
  parser.parse
59
59
  end
@@ -140,7 +140,7 @@ class YouTubeG
140
140
  class VideosFeedParser < VideoFeedParser #:nodoc:
141
141
 
142
142
  private
143
- def parse_content(content) #:nodoc:
143
+ def parse_content(content)
144
144
  doc = REXML::Document.new(content)
145
145
  feed = doc.elements["feed"]
146
146
 
@@ -2,40 +2,23 @@ class YouTubeG
2
2
  module Request #:nodoc:
3
3
  class BaseSearch #:nodoc:
4
4
  attr_reader :url
5
-
5
+
6
6
  private
7
-
8
- def base_url #:nodoc:
7
+
8
+ def base_url
9
9
  "http://gdata.youtube.com/feeds/api/"
10
10
  end
11
-
12
- def set_instance_variables( variables ) #:nodoc:
11
+
12
+ def set_instance_variables( variables )
13
13
  variables.each do |key, value|
14
14
  name = key.to_s
15
15
  instance_variable_set("@#{name}", value) if respond_to?(name)
16
16
  end
17
17
  end
18
-
19
- def build_query_params(params) #:nodoc:
20
- # nothing to do if there are no params
21
- return '' if (!params || params.empty?)
22
-
23
- # build up the query param string, tacking on every key/value
24
- # pair for which the value is non-nil
25
- u = '?'
26
- item_count = 0
27
- params.keys.sort.each do |key|
28
- value = params[key]
29
- next if value.nil?
30
-
31
- u << '&' if (item_count > 0)
32
- u << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
33
- item_count += 1
34
- end
35
-
36
- # if we found no non-nil values, we've got no params so just
37
- # return an empty string
38
- (item_count == 0) ? '' : u
18
+
19
+ def build_query_params(params)
20
+ qs = params.to_a.map { | k, v | v.nil? ? nil : "#{YouTubeG.esc(k)}=#{YouTubeG.esc(v)}" }.compact.sort.join('&')
21
+ qs.empty? ? '' : "?#{qs}"
39
22
  end
40
23
  end
41
24
 
@@ -22,11 +22,11 @@ class YouTubeG
22
22
 
23
23
  private
24
24
 
25
- def base_url #:nodoc:
25
+ def base_url
26
26
  super << "standardfeeds/"
27
27
  end
28
28
 
29
- def to_youtube_params #:nodoc:
29
+ def to_youtube_params
30
30
  {
31
31
  'max-results' => @max_results,
32
32
  'orderby' => @order_by,
@@ -26,11 +26,11 @@ class YouTubeG
26
26
 
27
27
  private
28
28
 
29
- def base_url #:nodoc:
29
+ def base_url
30
30
  super << "users/"
31
31
  end
32
32
 
33
- def to_youtube_params #:nodoc:
33
+ def to_youtube_params
34
34
  {
35
35
  'max-results' => @max_results,
36
36
  'orderby' => @order_by,
@@ -42,11 +42,11 @@ class YouTubeG
42
42
 
43
43
  private
44
44
 
45
- def base_url #:nodoc:
45
+ def base_url
46
46
  super << "videos"
47
47
  end
48
48
 
49
- def to_youtube_params #:nodoc:
49
+ def to_youtube_params
50
50
  {
51
51
  'max-results' => @max_results,
52
52
  'orderby' => @order_by,
@@ -62,7 +62,7 @@ class YouTubeG
62
62
  # Convert category symbols into strings and build the URL. GData requires categories to be capitalized.
63
63
  # Categories defined like: categories => { :include => [:news], :exclude => [:sports], :either => [..] }
64
64
  # or like: categories => [:news, :sports]
65
- def categories_to_params(categories) #:nodoc:
65
+ def categories_to_params(categories)
66
66
  if categories.respond_to?(:keys) and categories.respond_to?(:[])
67
67
  s = ""
68
68
  s << categories[:either].map { |c| c.to_s.capitalize }.join("%7C") << '/' if categories[:either]
@@ -76,15 +76,15 @@ class YouTubeG
76
76
 
77
77
  # Tags defined like: tags => { :include => [:football], :exclude => [:soccer], :either => [:polo, :tennis] }
78
78
  # or tags => [:football, :soccer]
79
- def tags_to_params(tags) #:nodoc:
79
+ def tags_to_params(tags)
80
80
  if tags.respond_to?(:keys) and tags.respond_to?(:[])
81
81
  s = ""
82
- s << tags[:either].map { |t| CGI.escape(t.to_s) }.join("%7C") << '/' if tags[:either]
83
- s << tags[:include].map { |t| CGI.escape(t.to_s) }.join("/") << '/' if tags[:include]
84
- s << ("-" << tags[:exclude].map { |t| CGI.escape(t.to_s) }.join("/-")) << '/' if tags[:exclude]
82
+ s << tags[:either].map { |t| YouTubeG.esc(t.to_s) }.join("%7C") << '/' if tags[:either]
83
+ s << tags[:include].map { |t| YouTubeG.esc(t.to_s) }.join("/") << '/' if tags[:include]
84
+ s << ("-" << tags[:exclude].map { |t| YouTubeG.esc(t.to_s) }.join("/-")) << '/' if tags[:exclude]
85
85
  s
86
86
  else
87
- tags.map { |t| CGI.escape(t.to_s) }.join("/") << '/'
87
+ tags.map { |t| YouTubeG.esc(t.to_s) }.join("/") << '/'
88
88
  end
89
89
  end
90
90
 
@@ -1,23 +1,25 @@
1
1
  class YouTubeG
2
2
 
3
3
  module Upload
4
- class UploadError < Exception; end
5
- class AuthenticationError < Exception; end
6
-
7
- # require 'youtube_g'
4
+ class UploadError < YouTubeG::Error; end
5
+ class AuthenticationError < YouTubeG::Error; end
6
+
7
+ # Implements video uploads/updates/deletions
8
8
  #
9
- # uploader = YouTubeG::Upload::VideoUpload.new("user", "pass", "dev-key")
10
- # uploader.upload File.open("test.m4v"), :title => 'test',
9
+ # require 'youtube_g'
10
+ #
11
+ # uploader = YouTubeG::Upload::VideoUpload.new("user", "pass", "dev-key")
12
+ # uploader.upload File.open("test.m4v"), :title => 'test',
11
13
  # :description => 'cool vid d00d',
12
14
  # :category => 'People',
13
15
  # :keywords => %w[cool blah test]
14
-
16
+ #
15
17
  class VideoUpload
16
-
18
+
17
19
  def initialize user, pass, dev_key, client_id = 'youtube_g'
18
20
  @user, @pass, @dev_key, @client_id = user, pass, dev_key, client_id
19
21
  end
20
-
22
+
21
23
  #
22
24
  # Upload "data" to youtube, where data is either an IO object or
23
25
  # raw file data.
@@ -32,97 +34,192 @@ class YouTubeG
32
34
  # Specifying :private will make the video private, otherwise it will be public.
33
35
  #
34
36
  # When one of the fields is invalid according to YouTube,
35
- # an UploadError will be returned. Its message contains a list of newline separated
37
+ # an UploadError will be raised. Its message contains a list of newline separated
36
38
  # errors, containing the key and its error code.
37
39
  #
38
40
  # When the authentication credentials are incorrect, an AuthenticationError will be raised.
39
41
  def upload data, opts = {}
40
- data = data.respond_to?(:read) ? data.read : data
41
42
  @opts = { :mime_type => 'video/mp4',
42
- :filename => Digest::MD5.hexdigest(data),
43
43
  :title => '',
44
44
  :description => '',
45
45
  :category => '',
46
46
  :keywords => [] }.merge(opts)
47
-
48
- uploadBody = generate_upload_body(boundary, video_xml, data)
49
-
50
- uploadHeader = {
47
+
48
+ @opts[:filename] ||= generate_uniq_filename_from(data)
49
+
50
+ post_body_io = generate_upload_io(video_xml, data)
51
+
52
+ upload_headers = authorization_headers.merge({
53
+ "Slug" => "#{@opts[:filename]}",
54
+ "Content-Type" => "multipart/related; boundary=#{boundary}",
55
+ "Content-Length" => "#{post_body_io.expected_length}", # required per YouTube spec
56
+ # "Transfer-Encoding" => "chunked" # We will stream instead of posting at once
57
+ })
58
+
59
+ path = '/feeds/api/users/%s/uploads' % @user
60
+
61
+ Net::HTTP.start(uploads_url) do | session |
62
+
63
+ # Use the chained IO as body so that Net::HTTP reads into the socket for us
64
+ post = Net::HTTP::Post.new(path, upload_headers)
65
+ post.body_stream = post_body_io
66
+
67
+ response = session.request(post)
68
+ raise_on_faulty_response(response)
69
+
70
+ return uploaded_video_id_from(response.body)
71
+ end
72
+ end
73
+
74
+ # Updates a video in YouTube. Requires:
75
+ # :title
76
+ # :description
77
+ # :category
78
+ # :keywords
79
+ # The following are optional attributes:
80
+ # :private
81
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
82
+ def update(video_id, options)
83
+ @opts = options
84
+
85
+ update_body = video_xml
86
+
87
+ update_header = authorization_headers.merge({
88
+ "Content-Type" => "application/atom+xml",
89
+ "Content-Length" => "#{update_body.length}",
90
+ })
91
+
92
+ update_url = "/feeds/api/users/#{@user}/uploads/#{video_id}"
93
+
94
+ Net::HTTP.start(base_url) do | session |
95
+ response = session.put(update_url, update_body, update_header)
96
+ raise_on_faulty_response(response)
97
+
98
+ return YouTubeG::Parser::VideoFeedParser.new(response.body).parse
99
+ end
100
+ end
101
+
102
+ # Delete a video on YouTube
103
+ def delete(video_id)
104
+ delete_header = authorization_headers.merge({
105
+ "Content-Type" => "application/atom+xml",
106
+ "Content-Length" => "0",
107
+ })
108
+
109
+ delete_url = "/feeds/api/users/#{@user}/uploads/#{video_id}"
110
+
111
+ Net::HTTP.start(base_url) do |session|
112
+ response = session.delete(delete_url, '', delete_header)
113
+ raise_on_faulty_response(response)
114
+ return true
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def uploads_url
121
+ ["uploads", base_url].join('.')
122
+ end
123
+
124
+ def base_url
125
+ "gdata.youtube.com"
126
+ end
127
+
128
+ def boundary
129
+ "An43094fu"
130
+ end
131
+
132
+ def authorization_headers
133
+ {
51
134
  "Authorization" => "GoogleLogin auth=#{auth_token}",
52
135
  "X-GData-Client" => "#{@client_id}",
53
- "X-GData-Key" => "key=#{@dev_key}",
54
- "Slug" => "#{@opts[:filename]}",
55
- "Content-Type" => "multipart/related; boundary=#{boundary}",
56
- "Content-Length" => "#{uploadBody.length}"
136
+ "X-GData-Key" => "key=#{@dev_key}"
57
137
  }
58
-
59
- Net::HTTP.start(base_url) do |upload|
60
- response = upload.post('/feeds/api/users/' << @user << '/uploads', uploadBody, uploadHeader)
61
- if response.code.to_i == 403
62
- raise AuthenticationError, response.body[/<TITLE>(.+)<\/TITLE>/, 1]
63
- elsif response.code.to_i != 201
64
- upload_error = ''
65
- xml = REXML::Document.new(response.body)
66
- errors = xml.elements["//errors"]
67
- errors.each do |error|
68
- location = error.elements["location"].text[/media:group\/media:(.*)\/text\(\)/,1]
69
- code = error.elements["code"].text
70
- upload_error << sprintf("%s: %s\r\n", location, code)
71
- end
72
- raise UploadError, upload_error
73
- end
74
- xml = REXML::Document.new(response.body)
75
- return xml.elements["//id"].text[/videos\/(.+)/, 1]
138
+ end
139
+
140
+ def parse_upload_error_from(string)
141
+ REXML::Document.new(string).elements["//errors"].inject('') do | all_faults, error|
142
+ location = error.elements["location"].text[/media:group\/media:(.*)\/text\(\)/,1]
143
+ code = error.elements["code"].text
144
+ all_faults + sprintf("%s: %s\n", location, code)
76
145
  end
77
-
78
146
  end
79
-
80
- private
81
-
82
- def base_url #:nodoc:
83
- "uploads.gdata.youtube.com"
147
+
148
+ def raise_on_faulty_response(response)
149
+ if response.code.to_i == 403
150
+ raise AuthenticationError, response.body[/<TITLE>(.+)<\/TITLE>/, 1]
151
+ elsif response.code.to_i != 200
152
+ raise UploadError, parse_upload_error_from(response.body)
153
+ end
84
154
  end
85
-
86
- def boundary #:nodoc:
87
- "An43094fu"
155
+
156
+ def uploaded_video_id_from(string)
157
+ xml = REXML::Document.new(string)
158
+ xml.elements["//id"].text[/videos\/(.+)/, 1]
88
159
  end
89
-
90
- def auth_token #:nodoc:
91
- unless @auth_token
160
+
161
+ # If data can be read, use the first 1024 bytes as filename. If data
162
+ # is a file, use path. If data is a string, checksum it
163
+ def generate_uniq_filename_from(data)
164
+ if data.respond_to?(:path)
165
+ Digest::MD5.hexdigest(data.path)
166
+ elsif data.respond_to?(:read)
167
+ chunk = data.read(1024)
168
+ data.rewind
169
+ Digest::MD5.hexdigest(chunk)
170
+ else
171
+ Digest::MD5.hexdigest(data)
172
+ end
173
+ end
174
+
175
+ def auth_token
176
+ @auth_token ||= begin
92
177
  http = Net::HTTP.new("www.google.com", 443)
93
178
  http.use_ssl = true
94
- body = "Email=#{CGI::escape @user}&Passwd=#{CGI::escape @pass}&service=youtube&source=#{CGI::escape @client_id}"
179
+ body = "Email=#{YouTubeG.esc @user}&Passwd=#{YouTubeG.esc @pass}&service=youtube&source=#{YouTubeG.esc @client_id}"
95
180
  response = http.post("/youtube/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
96
181
  raise UploadError, response.body[/Error=(.+)/,1] if response.code.to_i != 200
97
182
  @auth_token = response.body[/Auth=(.+)/, 1]
98
-
99
183
  end
100
- @auth_token
101
184
  end
102
-
103
- def video_xml #:nodoc:
104
- video_xml = ''
105
- video_xml << '<?xml version="1.0"?>'
106
- video_xml << '<entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">'
107
- video_xml << '<media:group>'
108
- video_xml << '<media:title type="plain">%s</media:title>' % @opts[:title]
109
- video_xml << '<media:description type="plain">%s</media:description>' % @opts[:description]
110
- video_xml << '<media:keywords>%s</media:keywords>' % @opts[:keywords].join(",")
111
- video_xml << '<media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">%s</media:category>' % @opts[:category]
112
- video_xml << '<yt:private/>' if @opts[:private]
113
- video_xml << '</media:group>'
114
- video_xml << '</entry>'
185
+
186
+ # TODO: isn't there a cleaner way to output top-notch XML without requiring stuff all over the place?
187
+ def video_xml
188
+ b = Builder::XML.new
189
+ b.instruct!
190
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:media' => "http://search.yahoo.com/mrss/", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
191
+ m.tag!("media:group") do | mg |
192
+ mg.tag!("media:title", :type => "plain") { @opts[:title] }
193
+ mg.tag!("media:description", :type => "plain") { @opts[:description] }
194
+ mg.tag!("media:keywords") { @opts[:keywords].join(",") }
195
+ mg.tag!('media:category', :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat") { @opts[:category] }
196
+ mg.tag!('yt:private') if @opts[:private]
197
+ end
198
+ end.to_s
115
199
  end
116
-
117
- def generate_upload_body(boundary, video_xml, data) #:nodoc:
118
- uploadBody = ""
119
- uploadBody << "--#{boundary}\r\n"
120
- uploadBody << "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n"
121
- uploadBody << video_xml
122
- uploadBody << "\r\n--#{boundary}\r\n"
123
- uploadBody << "Content-Type: #{@opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n"
124
- uploadBody << data
125
- uploadBody << "\r\n--#{boundary}--\r\n"
200
+
201
+ def generate_update_body(video_xml)
202
+ post_body = [
203
+ "--#{boundary}\r\n",
204
+ "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n",
205
+ video_xml,
206
+ "\r\n--#{boundary}\r\n",
207
+ ].join
208
+ end
209
+
210
+ def generate_upload_io(video_xml, data)
211
+ post_body = [
212
+ "--#{boundary}\r\n",
213
+ "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n",
214
+ video_xml,
215
+ "\r\n--#{boundary}\r\n",
216
+ "Content-Type: #{@opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n",
217
+ data,
218
+ "\r\n--#{boundary}--\r\n",
219
+ ]
220
+
221
+ # Use Greedy IO to not be limited by 1K chunks
222
+ YouTubeG::GreedyChainIO.new(post_body)
126
223
  end
127
224
 
128
225
  end
@@ -0,0 +1,3 @@
1
+ class YouTubeG
2
+ VERSION = '0.5.1'
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'pp'
4
+ require File.dirname(__FILE__) + '/../lib/youtube_g'
5
+
6
+ YouTubeG.logger.level = Logger::ERROR
@@ -0,0 +1,63 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class TestChainIO < Test::Unit::TestCase
4
+ def setup
5
+ @klass = YouTubeG::ChainIO # save typing
6
+ end
7
+
8
+ def test_should_support_read_from_one_io
9
+ io = @klass.new "abcd"
10
+ assert io.respond_to?(:read)
11
+ assert_equal "ab", io.read(2)
12
+ assert_equal "cd", io.read(2)
13
+ assert_equal false, io.read(2)
14
+ end
15
+
16
+ def test_should_skip_over_depleted_streams
17
+ io = @klass.new '', '', '', '', 'ab'
18
+ assert_equal 'ab', io.read(2)
19
+ end
20
+
21
+ def test_should_read_across_nultiple_streams_with_large_offset
22
+ io = @klass.new 'abc', '', 'def', '', 'ghij'
23
+ assert_equal 'abcdefgh', io.read(8)
24
+ end
25
+
26
+ def test_should_return_false_for_empty_items
27
+ io = @klass.new '', '', '', '', ''
28
+ assert_equal false, io.read(8)
29
+ end
30
+
31
+ def test_should_support_overzealous_read
32
+ io = @klass.new "ab"
33
+ assert_equal "ab", io.read(5000)
34
+ end
35
+
36
+ def test_should_predict_expected_length
37
+ io = @klass.new "ab", "cde"
38
+ assert_equal 5, io.expected_length
39
+ end
40
+
41
+ def test_should_predict_expected_length_with_prepositioned_io
42
+ first_buf = StringIO.new("ab")
43
+ first_buf.read(1)
44
+
45
+ io = @klass.new first_buf, "cde"
46
+ assert_equal 4, io.expected_length
47
+ end
48
+
49
+ def test_should_predict_expected_length_with_file_handle
50
+ test_size = File.size(__FILE__)
51
+ first_buf = StringIO.new("ab")
52
+ first_buf.read(1)
53
+
54
+ io = @klass.new File.open(__FILE__), first_buf
55
+ assert_equal test_size + 1, io.expected_length
56
+ end
57
+
58
+ def test_greedy
59
+ io = YouTubeG::GreedyChainIO.new("a" * (1024 * 513))
60
+ chunk = io.read(123)
61
+ assert_equal 1024 * 512, chunk.length, "Should have read the first 512 KB chunk at once instead"
62
+ end
63
+ end
@@ -1,8 +1,4 @@
1
- require 'rubygems'
2
- require 'test/unit'
3
- require 'pp'
4
-
5
- require 'youtube_g'
1
+ require File.dirname(__FILE__) + '/helper'
6
2
 
7
3
  class TestClient < Test::Unit::TestCase
8
4
  def setup
@@ -157,14 +153,13 @@ class TestClient < Test::Unit::TestCase
157
153
  assert_valid_video video
158
154
  end
159
155
 
160
- def test_should_disable_debug_if_debug_is_set_to_false
156
+ def test_should_always_return_a_logger
161
157
  @client = YouTubeG::Client.new
162
- assert_nil @client.logger
158
+ assert_not_nil @client.logger
163
159
  end
164
160
 
165
- def test_should_enable_logger_if_debug_is_true
166
- @client = YouTubeG::Client.new(true)
167
- assert_not_nil @client.logger
161
+ def test_should_not_bail_if_debug_is_true
162
+ assert_nothing_raised { YouTubeG::Client.new(true) }
168
163
  end
169
164
 
170
165
  def test_should_determine_if_nonembeddable_video_is_embeddable
@@ -1,8 +1,4 @@
1
- require 'rubygems'
2
- require 'test/unit'
3
- require 'pp'
4
-
5
- require 'youtube_g'
1
+ require File.dirname(__FILE__) + '/helper'
6
2
 
7
3
  class TestVideo < Test::Unit::TestCase
8
4
  def test_should_extract_unique_id_from_video_id
@@ -1,8 +1,4 @@
1
- require 'rubygems'
2
- require 'test/unit'
3
- require 'pp'
4
-
5
- require 'youtube_g'
1
+ require File.dirname(__FILE__) + '/helper'
6
2
 
7
3
  class TestVideoSearch < Test::Unit::TestCase
8
4
 
metadata CHANGED
@@ -1,34 +1,61 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tmm1-youtube-g
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane Vitarana
8
8
  - Walter Korman
9
9
  - Aman Gupta
10
10
  - Filip H.F. Slagter
11
+ - msp
11
12
  autorequire:
12
13
  bindir: bin
13
14
  cert_chain: []
14
15
 
15
- date: 2009-01-07 00:00:00 -08:00
16
+ date: 2009-03-02 00:00:00 -08:00
16
17
  default_executable:
17
- dependencies: []
18
-
19
- description: An object-oriented Ruby wrapper for the YouTube GData API
20
- email: ruby-youtube-library@googlegroups.com
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: builder
21
+ type: :runtime
22
+ version_requirement:
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: "0"
28
+ version:
29
+ - !ruby/object:Gem::Dependency
30
+ name: hoe
31
+ type: :development
32
+ version_requirement:
33
+ version_requirements: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: 1.8.3
38
+ version:
39
+ description: "youtube-g is a pure Ruby client for the YouTube GData API. It provides an easy way to access the latest YouTube video search results from your own programs. In comparison with the earlier Youtube search interfaces, this new API and library offers much-improved flexibility around executing complex search queries to obtain well-targeted video search results. More detail on the underlying source Google-provided API is available at: http://code.google.com/apis/youtube/overview.html"
40
+ email: shanev@gmail.com
21
41
  executables: []
22
42
 
23
43
  extensions: []
24
44
 
25
45
  extra_rdoc_files:
26
46
  - History.txt
47
+ - Manifest.txt
27
48
  - README.txt
49
+ - TODO.txt
28
50
  files:
29
51
  - History.txt
52
+ - Manifest.txt
53
+ - README.txt
54
+ - Rakefile
55
+ - TODO.txt
56
+ - lib/youtube_g.rb
57
+ - lib/youtube_g/chain_io.rb
30
58
  - lib/youtube_g/client.rb
31
- - lib/youtube_g/logger.rb
32
59
  - lib/youtube_g/model/author.rb
33
60
  - lib/youtube_g/model/category.rb
34
61
  - lib/youtube_g/model/contact.rb
@@ -46,16 +73,14 @@ files:
46
73
  - lib/youtube_g/request/video_search.rb
47
74
  - lib/youtube_g/request/video_upload.rb
48
75
  - lib/youtube_g/response/video_search.rb
49
- - lib/youtube_g.rb
50
- - Manifest.txt
51
- - README.txt
76
+ - lib/youtube_g/version.rb
77
+ - test/helper.rb
78
+ - test/test_chain_io.rb
52
79
  - test/test_client.rb
53
80
  - test/test_video.rb
54
81
  - test/test_video_search.rb
55
- - TODO.txt
56
- - youtube-g.gemspec
57
82
  has_rdoc: true
58
- homepage: http://youtube-g.rubyforge.org/
83
+ homepage: http://rubyforge.org/projects/youtube-g/
59
84
  post_install_message:
60
85
  rdoc_options:
61
86
  - --main
@@ -76,12 +101,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
101
  version:
77
102
  requirements: []
78
103
 
79
- rubyforge_project:
104
+ rubyforge_project: youtube-g
80
105
  rubygems_version: 1.2.0
81
106
  signing_key:
82
107
  specification_version: 2
83
- summary: An object-oriented Ruby wrapper for the YouTube GData API
108
+ summary: Ruby client for the YouTube GData API
84
109
  test_files:
110
+ - test/test_chain_io.rb
85
111
  - test/test_client.rb
86
112
  - test/test_video.rb
87
113
  - test/test_video_search.rb
@@ -1,25 +0,0 @@
1
- class YouTubeG
2
-
3
- # TODO: Why is this needed? Does this happen if running standalone w/o Rails?
4
- # Anyway, isn't it easier to debug w/o the really long timestamp & log level?
5
- # How often do you look at the timestamp and log level? Wouldn't it be nice to
6
- # see your logger output first?
7
-
8
- # Extension of the base ruby Logger class to restore the default log
9
- # level and timestamp formatting which is so rudely taken forcibly
10
- # away from us by the Rails app's use of the ActiveSupport library
11
- # that wholesale-ly modifies the Logger's format_message method.
12
- #
13
- class Logger < ::Logger
14
- private
15
- begin
16
- # restore original log formatting to un-screw the screwage that is
17
- # foisted upon us by the activesupport library's clean_logger.rb
18
- alias format_message old_format_message
19
-
20
- rescue NameError
21
- # nothing for now -- this means we didn't need to alias since the
22
- # method wasn't overridden
23
- end
24
- end
25
- end
@@ -1,52 +0,0 @@
1
- spec = Gem::Specification.new do |s|
2
- s.name = 'youtube-g'
3
- s.version = '0.5.0'
4
- s.date = '2009-01-07'
5
- s.summary = 'An object-oriented Ruby wrapper for the YouTube GData API'
6
- s.email = "ruby-youtube-library@googlegroups.com"
7
- s.homepage = "http://youtube-g.rubyforge.org/"
8
- s.description = "An object-oriented Ruby wrapper for the YouTube GData API"
9
- s.has_rdoc = true
10
- s.authors = ["Shane Vitarana", "Walter Korman", "Aman Gupta", "Filip H.F. Slagter"]
11
-
12
- # ruby -rpp -e "pp Dir['**/*.*'].map"
13
- s.files = [
14
- "History.txt",
15
- "lib/youtube_g/client.rb",
16
- "lib/youtube_g/logger.rb",
17
- "lib/youtube_g/model/author.rb",
18
- "lib/youtube_g/model/category.rb",
19
- "lib/youtube_g/model/contact.rb",
20
- "lib/youtube_g/model/content.rb",
21
- "lib/youtube_g/model/playlist.rb",
22
- "lib/youtube_g/model/rating.rb",
23
- "lib/youtube_g/model/thumbnail.rb",
24
- "lib/youtube_g/model/user.rb",
25
- "lib/youtube_g/model/video.rb",
26
- "lib/youtube_g/parser.rb",
27
- "lib/youtube_g/record.rb",
28
- "lib/youtube_g/request/base_search.rb",
29
- "lib/youtube_g/request/standard_search.rb",
30
- "lib/youtube_g/request/user_search.rb",
31
- "lib/youtube_g/request/video_search.rb",
32
- "lib/youtube_g/request/video_upload.rb",
33
- "lib/youtube_g/response/video_search.rb",
34
- "lib/youtube_g.rb",
35
- "Manifest.txt",
36
- "README.txt",
37
- "test/test_client.rb",
38
- "test/test_video.rb",
39
- "test/test_video_search.rb",
40
- "TODO.txt",
41
- "youtube-g.gemspec"
42
- ]
43
-
44
- s.test_files = [
45
- "test/test_client.rb",
46
- "test/test_video.rb",
47
- "test/test_video_search.rb"
48
- ]
49
-
50
- s.rdoc_options = ["--main", "README.txt"]
51
- s.extra_rdoc_files = ["History.txt", "README.txt"]
52
- end