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.
- data/History.txt +11 -0
- data/Manifest.txt +4 -1
- data/README.txt +19 -6
- data/Rakefile +24 -0
- data/lib/youtube_g.rb +59 -21
- data/lib/youtube_g/chain_io.rb +71 -0
- data/lib/youtube_g/client.rb +4 -4
- data/lib/youtube_g/parser.rb +1 -1
- data/lib/youtube_g/request/base_search.rb +9 -26
- data/lib/youtube_g/request/standard_search.rb +2 -2
- data/lib/youtube_g/request/user_search.rb +2 -2
- data/lib/youtube_g/request/video_search.rb +8 -8
- data/lib/youtube_g/request/video_upload.rb +173 -76
- data/lib/youtube_g/version.rb +3 -0
- data/test/helper.rb +6 -0
- data/test/test_chain_io.rb +63 -0
- data/test/test_client.rb +5 -10
- data/test/test_video.rb +1 -5
- data/test/test_video_search.rb +1 -5
- metadata +41 -15
- data/lib/youtube_g/logger.rb +0 -25
- data/youtube-g.gemspec +0 -52
data/History.txt
CHANGED
@@ -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)
|
data/Manifest.txt
CHANGED
@@ -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
|
-
|
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
|
-
*
|
68
|
+
* builder gem
|
56
69
|
|
57
70
|
== INSTALL:
|
58
71
|
|
data/Rakefile
ADDED
@@ -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
|
data/lib/youtube_g.rb
CHANGED
@@ -3,27 +3,65 @@ require 'open-uri'
|
|
3
3
|
require 'net/https'
|
4
4
|
require 'digest/md5'
|
5
5
|
require 'rexml/document'
|
6
|
-
require '
|
6
|
+
require 'builder'
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
28
|
-
|
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:
|
data/lib/youtube_g/client.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
class YouTubeG
|
2
2
|
class Client
|
3
|
-
|
3
|
+
include YouTubeG::Logging
|
4
4
|
|
5
|
-
|
6
|
-
|
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}]."
|
56
|
+
logger.debug "Submitting request [url=#{request.url}]."
|
57
57
|
parser = YouTubeG::Parser::VideosFeedParser.new(request.url)
|
58
58
|
parser.parse
|
59
59
|
end
|
data/lib/youtube_g/parser.rb
CHANGED
@@ -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
|
7
|
+
|
8
|
+
def base_url
|
9
9
|
"http://gdata.youtube.com/feeds/api/"
|
10
10
|
end
|
11
|
-
|
12
|
-
def set_instance_variables( variables )
|
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)
|
20
|
-
|
21
|
-
|
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
|
|
@@ -42,11 +42,11 @@ class YouTubeG
|
|
42
42
|
|
43
43
|
private
|
44
44
|
|
45
|
-
def base_url
|
45
|
+
def base_url
|
46
46
|
super << "videos"
|
47
47
|
end
|
48
48
|
|
49
|
-
def to_youtube_params
|
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)
|
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)
|
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|
|
83
|
-
s << tags[:include].map { |t|
|
84
|
-
s << ("-" << tags[:exclude].map { |t|
|
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|
|
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 <
|
5
|
-
class AuthenticationError <
|
6
|
-
|
7
|
-
#
|
4
|
+
class UploadError < YouTubeG::Error; end
|
5
|
+
class AuthenticationError < YouTubeG::Error; end
|
6
|
+
|
7
|
+
# Implements video uploads/updates/deletions
|
8
8
|
#
|
9
|
-
#
|
10
|
-
#
|
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
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
87
|
-
|
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
|
-
|
91
|
-
|
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=#{
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
data/test/helper.rb
ADDED
@@ -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
|
data/test/test_client.rb
CHANGED
@@ -1,8 +1,4 @@
|
|
1
|
-
require '
|
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
|
156
|
+
def test_should_always_return_a_logger
|
161
157
|
@client = YouTubeG::Client.new
|
162
|
-
|
158
|
+
assert_not_nil @client.logger
|
163
159
|
end
|
164
160
|
|
165
|
-
def
|
166
|
-
|
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
|
data/test/test_video.rb
CHANGED
data/test/test_video_search.rb
CHANGED
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.
|
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-
|
16
|
+
date: 2009-03-02 00:00:00 -08:00
|
16
17
|
default_executable:
|
17
|
-
dependencies:
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
-
|
51
|
-
-
|
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://
|
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:
|
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
|
data/lib/youtube_g/logger.rb
DELETED
@@ -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
|
data/youtube-g.gemspec
DELETED
@@ -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
|