slainer68_youtube_it 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +10 -0
- data/Gemfile.lock +27 -0
- data/Manifest.txt +37 -0
- data/README.rdoc +270 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/youtube_it/chain_io.rb +76 -0
- data/lib/youtube_it/client.rb +447 -0
- data/lib/youtube_it/middleware/faraday_authheader.rb +24 -0
- data/lib/youtube_it/middleware/faraday_oauth.rb +21 -0
- data/lib/youtube_it/middleware/faraday_oauth2.rb +13 -0
- data/lib/youtube_it/middleware/faraday_youtubeit.rb +30 -0
- data/lib/youtube_it/model/activity.rb +17 -0
- data/lib/youtube_it/model/author.rb +13 -0
- data/lib/youtube_it/model/category.rb +11 -0
- data/lib/youtube_it/model/comment.rb +16 -0
- data/lib/youtube_it/model/contact.rb +19 -0
- data/lib/youtube_it/model/content.rb +18 -0
- data/lib/youtube_it/model/message.rb +12 -0
- data/lib/youtube_it/model/playlist.rb +11 -0
- data/lib/youtube_it/model/rating.rb +23 -0
- data/lib/youtube_it/model/subscription.rb +7 -0
- data/lib/youtube_it/model/thumbnail.rb +17 -0
- data/lib/youtube_it/model/user.rb +27 -0
- data/lib/youtube_it/model/video.rb +239 -0
- data/lib/youtube_it/parser.rb +534 -0
- data/lib/youtube_it/record.rb +12 -0
- data/lib/youtube_it/request/base_search.rb +72 -0
- data/lib/youtube_it/request/error.rb +15 -0
- data/lib/youtube_it/request/standard_search.rb +43 -0
- data/lib/youtube_it/request/user_search.rb +47 -0
- data/lib/youtube_it/request/video_search.rb +102 -0
- data/lib/youtube_it/request/video_upload.rb +538 -0
- data/lib/youtube_it/response/video_search.rb +41 -0
- data/lib/youtube_it/version.rb +4 -0
- data/lib/youtube_it.rb +83 -0
- data/slainer68_youtube_it.gemspec +102 -0
- data/test/files/recorded_response.xml +1 -0
- data/test/files/youtube_video_response.xml +53 -0
- data/test/helper.rb +9 -0
- data/test/test.mov +0 -0
- data/test/test_chain_io.rb +63 -0
- data/test/test_client.rb +435 -0
- data/test/test_field_search.rb +48 -0
- data/test/test_video.rb +48 -0
- data/test/test_video_feed_parser.rb +271 -0
- data/test/test_video_search.rb +141 -0
- metadata +172 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
class YouTubeIt
|
2
|
+
module Request #:nodoc:
|
3
|
+
class BaseSearch #:nodoc:
|
4
|
+
attr_reader :url
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def base_url
|
9
|
+
"http://gdata.youtube.com/feeds/api/"
|
10
|
+
end
|
11
|
+
|
12
|
+
def set_instance_variables( variables )
|
13
|
+
variables.each do |key, value|
|
14
|
+
name = key.to_s
|
15
|
+
instance_variable_set("@#{name}", value) if respond_to?(name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_query_params(params)
|
20
|
+
qs = params.to_a.map { | k, v | v.nil? ? nil : "#{YouTubeIt.esc(k)}=#{YouTubeIt.esc(v)}" }.compact.sort.join('&')
|
21
|
+
qs.empty? ? '' : (@dev_key ? "?#{qs}&key=#{@dev_key}" : "?#{qs}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
module FieldSearch
|
27
|
+
def default_fields
|
28
|
+
"id,updated,openSearch:totalResults,openSearch:startIndex,openSearch:itemsPerPage"
|
29
|
+
end
|
30
|
+
|
31
|
+
def fields_to_params(fields)
|
32
|
+
return "" unless fields
|
33
|
+
|
34
|
+
fields_param = [default_fields]
|
35
|
+
|
36
|
+
if fields[:recorded]
|
37
|
+
if fields[:recorded].is_a? Range
|
38
|
+
fields_param << "entry[xs:date(yt:recorded) > xs:date('#{formatted_date(fields[:recorded].first)}') and xs:date(yt:recorded) < xs:date('#{formatted_date(fields[:recorded].last)}')]"
|
39
|
+
else
|
40
|
+
fields_param << "entry[xs:date(yt:recorded) = xs:date('#{formatted_date(fields[:recorded])}')]"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if fields[:published]
|
45
|
+
if fields[:published].is_a? Range
|
46
|
+
fields_param << "entry[xs:dateTime(published) > xs:dateTime('#{formatted_date(fields[:published].first)}T00:00:00') and xs:dateTime(published) < xs:dateTime('#{formatted_date(fields[:published].last)}T00:00:00')]"
|
47
|
+
else
|
48
|
+
fields_param << "entry[xs:date(published) = xs:date('#{formatted_date(fields[:published])}')]"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if fields[:view_count]
|
53
|
+
fields_param << "entry[yt:statistics/@viewCount > #{fields[:view_count]}]"
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
return "&fields=#{URI.escape(fields_param.join(","))}"
|
58
|
+
end
|
59
|
+
|
60
|
+
#youtube taked dates that look like 'YYYY-MM-DD'
|
61
|
+
def formatted_date(date)
|
62
|
+
return date if date.is_a? String
|
63
|
+
if date.respond_to? :strftime
|
64
|
+
date.strftime("%Y-%m-%d")
|
65
|
+
else
|
66
|
+
""
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class UploadError < YouTubeIt::Error
|
2
|
+
attr_reader :code
|
3
|
+
def initialize(msg, code = 0)
|
4
|
+
super(msg)
|
5
|
+
@code = code
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class AuthenticationError < YouTubeIt::Error
|
10
|
+
attr_reader :code
|
11
|
+
def initialize(msg, code = 0)
|
12
|
+
super(msg)
|
13
|
+
@code = code
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class YouTubeIt
|
2
|
+
module Request #:nodoc:
|
3
|
+
class StandardSearch < BaseSearch #:nodoc:
|
4
|
+
attr_reader :max_results # max_results
|
5
|
+
attr_reader :order_by # orderby, ([relevance], viewCount, published, rating)
|
6
|
+
attr_reader :offset # start-index
|
7
|
+
attr_reader :time # time
|
8
|
+
|
9
|
+
TYPES = [ :top_rated, :top_favorites, :most_viewed, :most_popular,
|
10
|
+
:most_recent, :most_discussed, :most_linked, :most_responded,
|
11
|
+
:recently_featured, :watch_on_mobile ]
|
12
|
+
|
13
|
+
def initialize(type, options={})
|
14
|
+
@dev_key = options[:dev_key] if options[:dev_key]
|
15
|
+
if TYPES.include?(type)
|
16
|
+
@max_results, @order_by, @offset, @time = nil
|
17
|
+
set_instance_variables(options)
|
18
|
+
@url = base_url + type.to_s << build_query_params(to_youtube_params)
|
19
|
+
else
|
20
|
+
raise "Invalid type, must be one of: #{ TYPES.map { |t| t.to_s }.join(", ") }"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def base_url
|
27
|
+
super << "standardfeeds/"
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_youtube_params
|
31
|
+
{
|
32
|
+
'max-results' => @max_results,
|
33
|
+
'orderby' => @order_by,
|
34
|
+
'start-index' => @offset,
|
35
|
+
'time' => @time,
|
36
|
+
'v' => 2
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class YouTubeIt
|
2
|
+
module Request #:nodoc:
|
3
|
+
class UserSearch < BaseSearch #:nodoc:
|
4
|
+
include FieldSearch
|
5
|
+
attr_reader :max_results # max_results
|
6
|
+
attr_reader :order_by # orderby, ([relevance], viewCount, published, rating)
|
7
|
+
attr_reader :offset # start-index
|
8
|
+
|
9
|
+
def initialize(params, options={})
|
10
|
+
@max_results, @order_by, @offset = nil
|
11
|
+
@url = base_url
|
12
|
+
@dev_key = options[:dev_key] if options[:dev_key]
|
13
|
+
if params == :favorites
|
14
|
+
@url << "#{options[:user]}/favorites"
|
15
|
+
set_instance_variables(options)
|
16
|
+
elsif params[:user] && options[:favorites]
|
17
|
+
@url << "#{params[:user]}/favorites"
|
18
|
+
set_instance_variables(params)
|
19
|
+
return
|
20
|
+
elsif params[:user]
|
21
|
+
@url << "#{params[:user]}/uploads"
|
22
|
+
set_instance_variables(params)
|
23
|
+
end
|
24
|
+
|
25
|
+
@url << build_query_params(to_youtube_params)
|
26
|
+
@url << fields_to_params(params.delete(:fields)) if params != :favorites && params[:fields]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def base_url
|
32
|
+
super << "users/"
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_youtube_params
|
36
|
+
{
|
37
|
+
'max-results' => @max_results,
|
38
|
+
'orderby' => @order_by,
|
39
|
+
'start-index' => @offset,
|
40
|
+
'v' => 2
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
class YouTubeIt
|
2
|
+
module Request #:nodoc:
|
3
|
+
class VideoSearch < BaseSearch #:nodoc:
|
4
|
+
include FieldSearch
|
5
|
+
|
6
|
+
# From here: http://code.google.com/apis/youtube/reference.html#yt_format
|
7
|
+
ONLY_EMBEDDABLE = 5
|
8
|
+
|
9
|
+
attr_reader :max_results # max_results
|
10
|
+
attr_reader :order_by # orderby, ([relevance], viewCount, published, rating)
|
11
|
+
attr_reader :offset # start-index
|
12
|
+
attr_reader :query # vq
|
13
|
+
attr_reader :response_format # alt, ([atom], rss, json)
|
14
|
+
attr_reader :tags # /-/tag1/tag2
|
15
|
+
attr_reader :categories # /-/Category1/Category2
|
16
|
+
attr_reader :video_format # format (1=mobile devices)
|
17
|
+
attr_reader :racy # racy ([exclude], include)
|
18
|
+
attr_reader :author
|
19
|
+
attr_reader :lang # lt
|
20
|
+
|
21
|
+
def initialize(params={})
|
22
|
+
# Initialize our various member data to avoid warnings and so we'll
|
23
|
+
# automatically fall back to the youtube api defaults
|
24
|
+
@max_results, @order_by,
|
25
|
+
@offset, @query,
|
26
|
+
@response_format, @video_format,
|
27
|
+
@racy, @author, @lang = nil
|
28
|
+
@url = base_url
|
29
|
+
@dev_key = params[:dev_key] if params[:dev_key]
|
30
|
+
|
31
|
+
# Return a single video (base_url + /T7YazwP8GtY)
|
32
|
+
return @url << "/" << params[:video_id] << "?v=2" if params[:video_id]
|
33
|
+
|
34
|
+
@url << "/-/" if (params[:categories] || params[:tags])
|
35
|
+
@url << categories_to_params(params.delete(:categories)) if params[:categories]
|
36
|
+
@url << tags_to_params(params.delete(:tags)) if params[:tags]
|
37
|
+
|
38
|
+
set_instance_variables(params)
|
39
|
+
|
40
|
+
if( params[ :only_embeddable ] )
|
41
|
+
@video_format = ONLY_EMBEDDABLE
|
42
|
+
end
|
43
|
+
|
44
|
+
@url << build_query_params(to_youtube_params)
|
45
|
+
@url << fields_to_params(params.delete(:fields)) if params[:fields]
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def base_url
|
51
|
+
super << "videos"
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_youtube_params
|
55
|
+
{
|
56
|
+
'max-results' => @max_results,
|
57
|
+
'orderby' => @order_by,
|
58
|
+
'start-index' => @offset,
|
59
|
+
'v' => 2,
|
60
|
+
'q' => @query,
|
61
|
+
'alt' => @response_format,
|
62
|
+
'format' => @video_format,
|
63
|
+
'racy' => @racy,
|
64
|
+
'author' => @author,
|
65
|
+
'lr' => @lang
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# Convert category symbols into strings and build the URL. GData requires categories to be capitalized.
|
71
|
+
# Categories defined like: categories => { :include => [:news], :exclude => [:sports], :either => [..] }
|
72
|
+
# or like: categories => [:news, :sports]
|
73
|
+
def categories_to_params(categories)
|
74
|
+
if categories.respond_to?(:keys) and categories.respond_to?(:[])
|
75
|
+
s = ""
|
76
|
+
s << categories[:either].map { |c| c.to_s.capitalize }.join("%7C") << '/' if categories[:either]
|
77
|
+
s << categories[:include].map { |c| c.to_s.capitalize }.join("/") << '/' if categories[:include]
|
78
|
+
s << ("-" << categories[:exclude].map { |c| c.to_s.capitalize }.join("/-")) << '/' if categories[:exclude]
|
79
|
+
s
|
80
|
+
else
|
81
|
+
categories.map { |c| c.to_s.capitalize }.join("/") << '/'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Tags defined like: tags => { :include => [:football], :exclude => [:soccer], :either => [:polo, :tennis] }
|
86
|
+
# or tags => [:football, :soccer]
|
87
|
+
def tags_to_params(tags)
|
88
|
+
if tags.respond_to?(:keys) and tags.respond_to?(:[])
|
89
|
+
s = ""
|
90
|
+
s << tags[:either].map { |t| YouTubeIt.esc(t.to_s) }.join("%7C") << '/' if tags[:either]
|
91
|
+
s << tags[:include].map { |t| YouTubeIt.esc(t.to_s) }.join("/") << '/' if tags[:include]
|
92
|
+
s << ("-" << tags[:exclude].map { |t| YouTubeIt.esc(t.to_s) }.join("/-")) << '/' if tags[:exclude]
|
93
|
+
s
|
94
|
+
else
|
95
|
+
tags.map { |t| YouTubeIt.esc(t.to_s) }.join("/") << '/'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
@@ -0,0 +1,538 @@
|
|
1
|
+
class YouTubeIt
|
2
|
+
module Upload
|
3
|
+
|
4
|
+
class UploadError < YouTubeIt::Error; end
|
5
|
+
|
6
|
+
class AuthenticationError < YouTubeIt::Error; end
|
7
|
+
|
8
|
+
# Implements video uploads/updates/deletions
|
9
|
+
#
|
10
|
+
# require 'youtube_it'
|
11
|
+
#
|
12
|
+
# uploader = YouTubeIt::Upload::VideoUpload.new("user", "pass", "dev-key")
|
13
|
+
# uploader.upload File.open("test.m4v"), :title => 'test',
|
14
|
+
# :description => 'cool vid d00d',
|
15
|
+
# :category => 'People',
|
16
|
+
# :keywords => %w[cool blah test]
|
17
|
+
#
|
18
|
+
class VideoUpload
|
19
|
+
include YouTubeIt::Logging
|
20
|
+
|
21
|
+
def initialize *params
|
22
|
+
if params.first.is_a?(Hash)
|
23
|
+
hash_options = params.first
|
24
|
+
@user = hash_options[:username]
|
25
|
+
@password = hash_options[:password]
|
26
|
+
@dev_key = hash_options[:dev_key]
|
27
|
+
@access_token = hash_options[:access_token]
|
28
|
+
@authsub_token = hash_options[:authsub_token]
|
29
|
+
@client_id = hash_options[:client_id] || "youtube_it"
|
30
|
+
@config_token = hash_options[:config_token]
|
31
|
+
else
|
32
|
+
puts "* warning: the method YouTubeIt::Upload::VideoUpload.new(username, password, dev_key) is depricated, use YouTubeIt::Upload::VideoUpload.new(:username => 'user', :password => 'passwd', :dev_key => 'dev_key')"
|
33
|
+
@user = params.shift
|
34
|
+
@password = params.shift
|
35
|
+
@dev_key = params.shift
|
36
|
+
@access_token = params.shift
|
37
|
+
@authsub_token = params.shift
|
38
|
+
@client_id = params.shift || "youtube_it"
|
39
|
+
@config_token = params.shift
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def enable_http_debugging
|
45
|
+
@http_debugging = true
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Upload "data" to youtube, where data is either an IO object or
|
50
|
+
# raw file data.
|
51
|
+
# The hash keys for opts (which specify video info) are as follows:
|
52
|
+
# :mime_type
|
53
|
+
# :filename
|
54
|
+
# :title
|
55
|
+
# :description
|
56
|
+
# :category
|
57
|
+
# :keywords
|
58
|
+
# :private
|
59
|
+
# New V2 api hash keys for accessControl:
|
60
|
+
# :rate
|
61
|
+
# :comment
|
62
|
+
# :commentVote
|
63
|
+
# :videoRespond
|
64
|
+
# :list
|
65
|
+
# :embed
|
66
|
+
# :syndicate
|
67
|
+
# Specifying :private will make the video private, otherwise it will be public.
|
68
|
+
#
|
69
|
+
# When one of the fields is invalid according to YouTube,
|
70
|
+
# an UploadError will be raised. Its message contains a list of newline separated
|
71
|
+
# errors, containing the key and its error code.
|
72
|
+
#
|
73
|
+
# When the authentication credentials are incorrect, an AuthenticationError will be raised.
|
74
|
+
def upload(data, opts = {})
|
75
|
+
@opts = { :mime_type => 'video/mp4',
|
76
|
+
:title => '',
|
77
|
+
:description => '',
|
78
|
+
:category => 'People',
|
79
|
+
:keywords => [] }.merge(opts)
|
80
|
+
|
81
|
+
@opts[:filename] ||= generate_uniq_filename_from(data)
|
82
|
+
|
83
|
+
post_body_io = generate_upload_io(video_xml, data)
|
84
|
+
|
85
|
+
upload_header = {
|
86
|
+
"Slug" => "#{@opts[:filename]}",
|
87
|
+
"Content-Type" => "multipart/related; boundary=#{boundary}",
|
88
|
+
"Content-Length" => "#{post_body_io.expected_length}",
|
89
|
+
}
|
90
|
+
|
91
|
+
upload_url = "/feeds/api/users/default/uploads"
|
92
|
+
response = yt_session(uploads_url).post(upload_url, post_body_io, upload_header)
|
93
|
+
|
94
|
+
return YouTubeIt::Parser::VideoFeedParser.new(response.body).parse
|
95
|
+
end
|
96
|
+
|
97
|
+
# Updates a video in YouTube. Requires:
|
98
|
+
# :title
|
99
|
+
# :description
|
100
|
+
# :category
|
101
|
+
# :keywords
|
102
|
+
# The following are optional attributes:
|
103
|
+
# :private
|
104
|
+
# When the authentication credentials are incorrect, an AuthenticationError will be raised.
|
105
|
+
def update(video_id, options)
|
106
|
+
@opts = { :title => '',
|
107
|
+
:description => '',
|
108
|
+
:category => 'People',
|
109
|
+
:keywords => [] }.merge(options)
|
110
|
+
|
111
|
+
update_body = video_xml
|
112
|
+
update_url = "/feeds/api/users/default/uploads/%s" % video_id
|
113
|
+
response = yt_session.put(update_url, update_body)
|
114
|
+
|
115
|
+
return YouTubeIt::Parser::VideoFeedParser.new(response.body).parse
|
116
|
+
end
|
117
|
+
|
118
|
+
# Fetches the currently authenticated user's contacts (i.e. friends).
|
119
|
+
# When the authentication credentials are incorrect, an AuthenticationError will be raised.
|
120
|
+
def get_my_contacts(opts)
|
121
|
+
contacts_url = "/feeds/api/users/default/contacts?v=2"
|
122
|
+
contacts_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
|
123
|
+
response = yt_session.get(contacts_url)
|
124
|
+
|
125
|
+
return YouTubeIt::Parser::ContactsParser.new(response).parse
|
126
|
+
end
|
127
|
+
|
128
|
+
def send_message(opts)
|
129
|
+
message_body = message_xml_for(opts)
|
130
|
+
message_url = "/feeds/api/users/%s/inbox" % opts[:recipient_id]
|
131
|
+
response = yt_session.post(message_url, message_body)
|
132
|
+
|
133
|
+
return {:code => response.status, :body => response.body}
|
134
|
+
end
|
135
|
+
|
136
|
+
# Fetches the currently authenticated user's messages (i.e. inbox).
|
137
|
+
# When the authentication credentials are incorrect, an AuthenticationError will be raised.
|
138
|
+
def get_my_messages(opts)
|
139
|
+
messages_url = "/feeds/api/users/default/inbox"
|
140
|
+
messages_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
|
141
|
+
response = yt_session.get(messages_url)
|
142
|
+
|
143
|
+
return YouTubeIt::Parser::MessagesParser.new(response).parse
|
144
|
+
end
|
145
|
+
|
146
|
+
# Fetches the data of a video, which may be private. The video must be owned by this user.
|
147
|
+
# When the authentication credentials are incorrect, an AuthenticationError will be raised.
|
148
|
+
def get_my_video(video_id)
|
149
|
+
get_url = "/feeds/api/users/default/uploads/%s" % video_id
|
150
|
+
response = yt_session.get(get_url)
|
151
|
+
|
152
|
+
return YouTubeIt::Parser::VideoFeedParser.new(response.body).parse
|
153
|
+
end
|
154
|
+
|
155
|
+
# Fetches the data of the videos of the current user, which may be private.
|
156
|
+
# When the authentication credentials are incorrect, an AuthenticationError will be raised.
|
157
|
+
def get_my_videos(opts)
|
158
|
+
max_results = opts[:per_page] || 50
|
159
|
+
start_index = ((opts[:page] || 1) -1) * max_results +1
|
160
|
+
get_url = "/feeds/api/users/default/uploads?max-results=#{max_results}&start-index=#{start_index}"
|
161
|
+
response = yt_session.get(get_url)
|
162
|
+
|
163
|
+
return YouTubeIt::Parser::VideosFeedParser.new(response.body).parse
|
164
|
+
end
|
165
|
+
|
166
|
+
# Delete a video on YouTube
|
167
|
+
def delete(video_id)
|
168
|
+
delete_url = "/feeds/api/users/default/uploads/%s" % video_id
|
169
|
+
response = yt_session.delete(delete_url)
|
170
|
+
|
171
|
+
return true
|
172
|
+
end
|
173
|
+
|
174
|
+
# Delete a video message
|
175
|
+
def delete_message(message_id)
|
176
|
+
delete_url = "/feeds/api/users/default/inbox/%s" % message_id
|
177
|
+
response = yt_session.delete(delete_url)
|
178
|
+
|
179
|
+
return true
|
180
|
+
end
|
181
|
+
|
182
|
+
def get_upload_token(options, nexturl)
|
183
|
+
@opts = options
|
184
|
+
token_body = video_xml
|
185
|
+
token_url = "/action/GetUploadToken"
|
186
|
+
response = yt_session.post(token_url, token_body)
|
187
|
+
|
188
|
+
return {:url => "#{response.body[/<url>(.+)<\/url>/, 1]}?nexturl=#{nexturl}",
|
189
|
+
:token => response.body[/<token>(.+)<\/token>/, 1]}
|
190
|
+
end
|
191
|
+
|
192
|
+
def add_comment(video_id, comment)
|
193
|
+
comment_body = video_xml_for(:comment => comment)
|
194
|
+
comment_url = "/feeds/api/videos/%s/comments" % video_id
|
195
|
+
response = yt_session.post(comment_url, comment_body)
|
196
|
+
|
197
|
+
return {:code => response.status, :body => response.body}
|
198
|
+
end
|
199
|
+
|
200
|
+
def comments(video_id, opts = {})
|
201
|
+
comment_url = "/feeds/api/videos/%s/comments?" % video_id
|
202
|
+
comment_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
|
203
|
+
response = yt_session.get(comment_url)
|
204
|
+
return YouTubeIt::Parser::CommentsFeedParser.new(response).parse
|
205
|
+
end
|
206
|
+
|
207
|
+
def add_favorite(video_id)
|
208
|
+
favorite_body = video_xml_for(:favorite => video_id)
|
209
|
+
favorite_url = "/feeds/api/users/default/favorites"
|
210
|
+
response = yt_session.post(favorite_url, favorite_body)
|
211
|
+
|
212
|
+
return {:code => response.status, :body => response.body}
|
213
|
+
end
|
214
|
+
|
215
|
+
def delete_favorite(video_id)
|
216
|
+
favorite_header = {
|
217
|
+
"GData-Version" => "1",
|
218
|
+
}
|
219
|
+
favorite_url = "/feeds/api/users/default/favorites/%s" % video_id
|
220
|
+
response = yt_session.delete(favorite_url, favorite_header)
|
221
|
+
|
222
|
+
return true
|
223
|
+
end
|
224
|
+
|
225
|
+
def profile(user)
|
226
|
+
profile_url = "/feeds/api/users/%s?v=2" % (user ? user : "default")
|
227
|
+
response = yt_session.get(profile_url)
|
228
|
+
|
229
|
+
return YouTubeIt::Parser::ProfileFeedParser.new(response).parse
|
230
|
+
end
|
231
|
+
|
232
|
+
# Return's a user's activity feed.
|
233
|
+
def get_activity(user, opts)
|
234
|
+
activity_url = "/feeds/api/events?author=%s&v=2&" % (user ? user : "default")
|
235
|
+
activity_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
|
236
|
+
response = yt_session.get(activity_url)
|
237
|
+
|
238
|
+
return YouTubeIt::Parser::ActivityParser.new(response).parse
|
239
|
+
end
|
240
|
+
|
241
|
+
def playlist(playlist_id)
|
242
|
+
playlist_url = "/feeds/api/playlists/%s?v=2" % playlist_id
|
243
|
+
response = yt_session.get(playlist_url)
|
244
|
+
|
245
|
+
return YouTubeIt::Parser::PlaylistFeedParser.new(response).parse
|
246
|
+
end
|
247
|
+
|
248
|
+
def playlists(user)
|
249
|
+
playlist_url = "/feeds/api/users/%s/playlists?v=2" % (user ? user : "default")
|
250
|
+
response = yt_session.get(playlist_url)
|
251
|
+
|
252
|
+
return YouTubeIt::Parser::PlaylistsFeedParser.new(response).parse
|
253
|
+
end
|
254
|
+
|
255
|
+
def add_playlist(options)
|
256
|
+
playlist_body = video_xml_for_playlist(options)
|
257
|
+
playlist_url = "/feeds/api/users/default/playlists"
|
258
|
+
response = yt_session.post(playlist_url, playlist_body)
|
259
|
+
|
260
|
+
return YouTubeIt::Parser::PlaylistFeedParser.new(response).parse
|
261
|
+
end
|
262
|
+
|
263
|
+
def add_video_to_playlist(playlist_id, video_id)
|
264
|
+
playlist_body = video_xml_for(:playlist => video_id)
|
265
|
+
playlist_url = "/feeds/api/playlists/%s" % playlist_id
|
266
|
+
response = yt_session.post(playlist_url, playlist_body)
|
267
|
+
|
268
|
+
return {:code => response.status, :body => response.body, :playlist_entry_id => playlist_entry_id_from_playlist(response.body)}
|
269
|
+
end
|
270
|
+
|
271
|
+
def update_playlist(playlist_id, options)
|
272
|
+
playlist_body = video_xml_for_playlist(options)
|
273
|
+
playlist_url = "/feeds/api/users/default/playlists/%s" % playlist_id
|
274
|
+
response = yt_session.put(playlist_url, playlist_body)
|
275
|
+
|
276
|
+
return YouTubeIt::Parser::PlaylistFeedParser.new(response).parse
|
277
|
+
end
|
278
|
+
|
279
|
+
def delete_video_from_playlist(playlist_id, playlist_entry_id)
|
280
|
+
playlist_url = "/feeds/api/playlists/%s/%s" % [playlist_id, playlist_entry_id]
|
281
|
+
response = yt_session.delete(playlist_url)
|
282
|
+
|
283
|
+
return true
|
284
|
+
end
|
285
|
+
|
286
|
+
def delete_playlist(playlist_id)
|
287
|
+
playlist_url = "/feeds/api/users/default/playlists/%s" % playlist_id
|
288
|
+
response = yt_session.delete(playlist_url)
|
289
|
+
|
290
|
+
return true
|
291
|
+
end
|
292
|
+
|
293
|
+
def rate_video(video_id, rating)
|
294
|
+
rating_body = video_xml_for(:rating => rating)
|
295
|
+
rating_url = "/feeds/api/videos/#{video_id}/ratings"
|
296
|
+
response = yt_session.post(rating_url, rating_body)
|
297
|
+
|
298
|
+
return {:code => response.status, :body => response.body}
|
299
|
+
end
|
300
|
+
|
301
|
+
def subscriptions(user)
|
302
|
+
subscription_url = "/feeds/api/users/%s/subscriptions?v=2" % (user ? user : "default")
|
303
|
+
response = yt_session.get(subscription_url)
|
304
|
+
|
305
|
+
return YouTubeIt::Parser::SubscriptionFeedParser.new(response).parse
|
306
|
+
end
|
307
|
+
|
308
|
+
def subscribe_channel(channel_name)
|
309
|
+
subscribe_body = video_xml_for(:subscribe => channel_name)
|
310
|
+
subscribe_url = "/feeds/api/users/default/subscriptions"
|
311
|
+
response = yt_session.post(subscribe_url, subscribe_body)
|
312
|
+
|
313
|
+
return {:code => response.status, :body => response.body}
|
314
|
+
end
|
315
|
+
|
316
|
+
def unsubscribe_channel(subscription_id)
|
317
|
+
unsubscribe_url = "/feeds/api/users/default/subscriptions/%s" % subscription_id
|
318
|
+
response = yt_session.delete(unsubscribe_url)
|
319
|
+
|
320
|
+
return {:code => response.status, :body => response.body}
|
321
|
+
end
|
322
|
+
|
323
|
+
def favorites(user, opts = {})
|
324
|
+
favorite_url = "/feeds/api/users/%s/favorites#{opts.empty? ? '' : '?#{opts.to_param}'}" % (user ? user : "default")
|
325
|
+
response = yt_session.get(favorite_url)
|
326
|
+
|
327
|
+
return YouTubeIt::Parser::VideosFeedParser.new(response.body).parse
|
328
|
+
end
|
329
|
+
|
330
|
+
def get_current_user
|
331
|
+
current_user_url = "/feeds/api/users/default"
|
332
|
+
response = yt_session.get(current_user_url)
|
333
|
+
|
334
|
+
return REXML::Document.new(response.body).elements["entry"].elements['author'].elements['name'].text
|
335
|
+
end
|
336
|
+
|
337
|
+
def add_response(original_video_id, response_video_id)
|
338
|
+
response_body = video_xml_for(:response => response_video_id)
|
339
|
+
response_url = "/feeds/api/videos/%s/responses" % original_video_id
|
340
|
+
response = yt_session.post(response_url, response_body)
|
341
|
+
|
342
|
+
return {:code => response.status, :body => response.body}
|
343
|
+
end
|
344
|
+
|
345
|
+
def delete_response(original_video_id, response_video_id)
|
346
|
+
response_url = "/feeds/api/videos/%s/responses/%s" % [original_video_id, response_video_id]
|
347
|
+
response = yt_session.delete(response_url)
|
348
|
+
|
349
|
+
return {:code => response.status, :body => response.body}
|
350
|
+
end
|
351
|
+
|
352
|
+
|
353
|
+
private
|
354
|
+
|
355
|
+
def uploads_url
|
356
|
+
["http://uploads", base_url.sub("http://","")].join('.')
|
357
|
+
end
|
358
|
+
|
359
|
+
def base_url
|
360
|
+
"http://gdata.youtube.com"
|
361
|
+
end
|
362
|
+
|
363
|
+
def boundary
|
364
|
+
"An43094fu"
|
365
|
+
end
|
366
|
+
|
367
|
+
def authorization_headers
|
368
|
+
header = {"X-GData-Client" => "#{@client_id}"}
|
369
|
+
header.merge!("X-GData-Key" => "key=#{@dev_key}") if @dev_key
|
370
|
+
if @authsub_token
|
371
|
+
header.merge!("Authorization" => "AuthSub token=#{@authsub_token}")
|
372
|
+
elsif @access_token.nil? && @authsub_token.nil? && @user
|
373
|
+
header.merge!("Authorization" => "GoogleLogin auth=#{auth_token}")
|
374
|
+
end
|
375
|
+
header
|
376
|
+
end
|
377
|
+
|
378
|
+
def parse_upload_error_from(string)
|
379
|
+
begin
|
380
|
+
REXML::Document.new(string).elements["//errors"].inject('') do | all_faults, error|
|
381
|
+
if error.elements["internalReason"]
|
382
|
+
msg_error = error.elements["internalReason"].text
|
383
|
+
elsif error.elements["location"]
|
384
|
+
msg_error = error.elements["location"].text[/media:group\/media:(.*)\/text\(\)/,1]
|
385
|
+
else
|
386
|
+
msg_error = "Unspecified error"
|
387
|
+
end
|
388
|
+
code = error.elements["code"].text if error.elements["code"]
|
389
|
+
all_faults + sprintf("%s: %s\n", msg_error, code)
|
390
|
+
end
|
391
|
+
rescue
|
392
|
+
string[/<TITLE>(.+)<\/TITLE>/, 1] || string
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def raise_on_faulty_response(response)
|
397
|
+
response_code = response.code.to_i
|
398
|
+
msg = parse_upload_error_from(response.body.gsub(/\n/, ''))
|
399
|
+
|
400
|
+
if response_code == 403 || response_code == 401
|
401
|
+
#if response_code / 10 == 40
|
402
|
+
raise AuthenticationError.new(msg, response_code)
|
403
|
+
elsif response_code / 10 != 20 # Response in 20x means success
|
404
|
+
raise UploadError.new(msg, response_code)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def uploaded_video_id_from(string)
|
409
|
+
xml = REXML::Document.new(string)
|
410
|
+
xml.elements["//id"].text[/videos\/(.+)/, 1]
|
411
|
+
end
|
412
|
+
|
413
|
+
def playlist_id_from(string)
|
414
|
+
xml = REXML::Document.new(string)
|
415
|
+
entry = xml.elements["entry"]
|
416
|
+
entry.elements["id"].text[/playlist([^<]+)/, 1].sub(':','')
|
417
|
+
end
|
418
|
+
|
419
|
+
# If data can be read, use the first 1024 bytes as filename. If data
|
420
|
+
# is a file, use path. If data is a string, checksum it
|
421
|
+
def generate_uniq_filename_from(data)
|
422
|
+
if data.respond_to?(:path)
|
423
|
+
Digest::MD5.hexdigest(data.path)
|
424
|
+
elsif data.respond_to?(:read)
|
425
|
+
chunk = data.read(1024)
|
426
|
+
data.rewind
|
427
|
+
Digest::MD5.hexdigest(chunk)
|
428
|
+
else
|
429
|
+
Digest::MD5.hexdigest(data)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def auth_token
|
434
|
+
@auth_token ||= begin
|
435
|
+
http = Faraday.new("https://www.google.com", :ssl => {:verify => false})
|
436
|
+
body = "Email=#{YouTubeIt.esc @user}&Passwd=#{YouTubeIt.esc @password}&service=youtube&source=#{YouTubeIt.esc @client_id}"
|
437
|
+
response = http.post("/youtube/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
|
438
|
+
raise ::AuthenticationError.new(response.body[/Error=(.+)/,1], response.status.to_i) if response.status.to_i != 200
|
439
|
+
@auth_token = response.body[/Auth=(.+)/, 1]
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# TODO: isn't there a cleaner way to output top-notch XML without requiring stuff all over the place?
|
444
|
+
def video_xml
|
445
|
+
b = Builder::XmlMarkup.new
|
446
|
+
b.instruct!
|
447
|
+
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 |
|
448
|
+
m.tag!("media:group") do | mg |
|
449
|
+
mg.tag!("media:title", @opts[:title], :type => "plain")
|
450
|
+
mg.tag!("media:description", @opts[:description], :type => "plain")
|
451
|
+
mg.tag!("media:keywords", @opts[:keywords].join(","))
|
452
|
+
mg.tag!('media:category', @opts[:category], :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat")
|
453
|
+
mg.tag!('yt:private') if @opts[:private]
|
454
|
+
mg.tag!('media:category', @opts[:dev_tag], :scheme => "http://gdata.youtube.com/schemas/2007/developertags.cat") if @opts[:dev_tag]
|
455
|
+
end
|
456
|
+
m.tag!("yt:accessControl", :action => "rate", :permission => @opts[:rate]) if @opts[:rate]
|
457
|
+
m.tag!("yt:accessControl", :action => "comment", :permission => @opts[:comment]) if @opts[:comment]
|
458
|
+
m.tag!("yt:accessControl", :action => "commentVote", :permission => @opts[:commentVote]) if @opts[:commentVote]
|
459
|
+
m.tag!("yt:accessControl", :action => "videoRespond", :permission => @opts[:videoRespond]) if @opts[:videoRespond]
|
460
|
+
m.tag!("yt:accessControl", :action => "list", :permission => @opts[:list]) if @opts[:list]
|
461
|
+
m.tag!("yt:accessControl", :action => "embed", :permission => @opts[:embed]) if @opts[:embed]
|
462
|
+
m.tag!("yt:accessControl", :action => "syndicate", :permission => @opts[:syndicate]) if @opts[:syndicate]
|
463
|
+
end.to_s
|
464
|
+
end
|
465
|
+
|
466
|
+
def video_xml_for(data)
|
467
|
+
b = Builder::XmlMarkup.new
|
468
|
+
b.instruct!
|
469
|
+
b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
|
470
|
+
m.content(data[:comment]) if data[:comment]
|
471
|
+
m.id(data[:favorite] || data[:playlist] || data[:response]) if data[:favorite] || data[:playlist] || data[:response]
|
472
|
+
m.tag!("yt:rating", :value => data[:rating]) if data[:rating]
|
473
|
+
if(data[:subscribe])
|
474
|
+
m.category(:scheme => "http://gdata.youtube.com/schemas/2007/subscriptiontypes.cat", :term => "channel")
|
475
|
+
m.tag!("yt:username", data[:subscribe])
|
476
|
+
end
|
477
|
+
end.to_s
|
478
|
+
end
|
479
|
+
|
480
|
+
def message_xml_for(data)
|
481
|
+
b = Builder::XmlMarkup.new
|
482
|
+
b.instruct!
|
483
|
+
b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
|
484
|
+
m.id(data[:vedio_id]) #if data[:vedio_id]
|
485
|
+
m.title(data[:title]) if data[:title]
|
486
|
+
m.summary(data[:message])
|
487
|
+
end.to_s
|
488
|
+
end
|
489
|
+
|
490
|
+
def video_xml_for_playlist(data)
|
491
|
+
b = Builder::XmlMarkup.new
|
492
|
+
b.instruct!
|
493
|
+
b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
|
494
|
+
m.title(data[:title]) if data[:title]
|
495
|
+
m.summary(data[:description] || data[:summary]) if data[:description] || data[:summary]
|
496
|
+
m.tag!('yt:private') if data[:private]
|
497
|
+
end.to_s
|
498
|
+
end
|
499
|
+
|
500
|
+
def generate_upload_io(video_xml, data)
|
501
|
+
post_body = [
|
502
|
+
"--#{boundary}\r\n",
|
503
|
+
"Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n",
|
504
|
+
video_xml,
|
505
|
+
"\r\n--#{boundary}\r\n",
|
506
|
+
"Content-Type: #{@opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n",
|
507
|
+
data,
|
508
|
+
"\r\n--#{boundary}--\r\n",
|
509
|
+
]
|
510
|
+
|
511
|
+
# Use Greedy IO to not be limited by 1K chunks
|
512
|
+
YouTubeIt::GreedyChainIO.new(post_body)
|
513
|
+
end
|
514
|
+
|
515
|
+
def playlist_entry_id_from_playlist(string)
|
516
|
+
playlist_xml = REXML::Document.new(string)
|
517
|
+
playlist_xml.elements.each("/entry") do |item|
|
518
|
+
return item.elements["id"].text[/^.*:([^:]+)$/,1]
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
def yt_session(url = nil)
|
523
|
+
Faraday.new(:url => (url ? url : base_url), :ssl => {:verify => false}) do |builder|
|
524
|
+
if @access_token
|
525
|
+
if @config_token
|
526
|
+
builder.use Faraday::Request::OAuth, @config_token
|
527
|
+
else
|
528
|
+
builder.use Faraday::Request::OAuth2, @access_token
|
529
|
+
end
|
530
|
+
end
|
531
|
+
builder.use Faraday::Request::AuthHeader, authorization_headers
|
532
|
+
builder.use Faraday::Response::YouTubeIt
|
533
|
+
builder.adapter Faraday.default_adapter
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|