slainer68_youtube_it 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|