msp-youtube-g 0.4.8.1 → 0.4.8.3
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +2 -1
- data/lib/youtube_g.rb +1 -1
- data/lib/youtube_g/client.rb +23 -5
- data/lib/youtube_g/model/video.rb +16 -1
- data/lib/youtube_g/parser.rb +12 -3
- data/lib/youtube_g/request/video_upload.rb +5 -1
- data/test/status.xml +47 -0
- data/test/test_parser.rb +25 -3
- data/test/test_video.rb +26 -0
- metadata +3 -1
data/History.txt
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
== trunk
|
1
|
+
== trunk
|
2
|
+
* Create a method to check the status of the uploaded video by status URL [msp]
|
2
3
|
* Move network dependent tests to integration-test [msp]
|
3
4
|
* Create a parser and a model for upload errors [msp]
|
4
5
|
* Stub driven tests for VideoFeedParser, VideosFeedParser, UploadErrorParser [msp]
|
data/lib/youtube_g.rb
CHANGED
data/lib/youtube_g/client.rb
CHANGED
@@ -61,16 +61,34 @@ class YouTubeG
|
|
61
61
|
# Retrieves a single YouTube video.
|
62
62
|
#
|
63
63
|
# === Parameters
|
64
|
-
# vid<String>:: The ID or URL of the video that you'd like to retrieve.
|
64
|
+
# vid<String>:: The ID or URL of the video that you'd like to retrieve. If your uncertain
|
65
|
+
# of the processed state use video_processed_by_youtube? first else this will fail.
|
65
66
|
#
|
66
67
|
# === Returns
|
67
68
|
# YouTubeG::Model::Video
|
68
69
|
def video_by(vid)
|
69
|
-
video_id = vid =~ /^http/ ? vid : "http://gdata.youtube.com/feeds/videos/#{vid}"
|
70
|
+
video_id = vid =~ /^http/ ? vid : "http://gdata.youtube.com/feeds/api/videos/#{vid}"
|
70
71
|
parser = YouTubeG::Parser::VideoFeedParser.new(video_id)
|
71
|
-
parser.parse
|
72
|
-
end
|
73
|
-
|
72
|
+
parser.parse
|
73
|
+
end
|
74
|
+
|
75
|
+
# Checks the status of a users uploaded video.
|
76
|
+
#
|
77
|
+
# === Parameters
|
78
|
+
# status_url<String>:: The URL returned by YT when the video was uploaded e.g.
|
79
|
+
#
|
80
|
+
# http://gdata.youtube.com/feeds/api/users/speechboxmatt/uploads/QR1sWOVqxoM
|
81
|
+
#
|
82
|
+
# This is stored on upload as Video.status_url
|
83
|
+
# === Returns
|
84
|
+
# Boolean
|
85
|
+
def video_by_status_url(status_url)
|
86
|
+
parser = YouTubeG::Parser::VideoFeedParser.new(status_url)
|
87
|
+
video = parser.parse
|
88
|
+
return video
|
89
|
+
end
|
90
|
+
|
91
|
+
|
74
92
|
private
|
75
93
|
|
76
94
|
def calculate_offset(page, per_page)
|
@@ -59,7 +59,10 @@ class YouTubeG
|
|
59
59
|
# Used in video uploads only to check the state of the upload
|
60
60
|
class AppControl < YouTubeG::Record
|
61
61
|
attr_reader :draft
|
62
|
-
attr_reader :state
|
62
|
+
attr_reader :state
|
63
|
+
attr_reader :reason
|
64
|
+
attr_reader :help_url
|
65
|
+
attr_reader :description
|
63
66
|
end
|
64
67
|
|
65
68
|
# *Fixnum*:: Duration of a video in seconds.
|
@@ -113,6 +116,9 @@ class YouTubeG
|
|
113
116
|
# *String*:: The link to watch the URL on YouTubes website.
|
114
117
|
attr_reader :player_url
|
115
118
|
|
119
|
+
# *String*:: The URL to check the processing status of the video at YT.
|
120
|
+
attr_reader :status_url
|
121
|
+
|
116
122
|
# YouTubeG::Model::Rating:: Information about the videos rating.
|
117
123
|
attr_reader :rating
|
118
124
|
|
@@ -187,6 +193,15 @@ EDOC
|
|
187
193
|
# String: Absolute URL for embedding video
|
188
194
|
def embed_url
|
189
195
|
@player_url.sub('watch?', '').sub('=', '/')
|
196
|
+
end
|
197
|
+
|
198
|
+
# Has this vid been processed by YT?
|
199
|
+
#
|
200
|
+
# === Returns
|
201
|
+
# Boolean
|
202
|
+
def processed?
|
203
|
+
return true if self.app_control == nil
|
204
|
+
return false if self.app_control.state.upcase == 'PROCESSING' || self.app_control.state.upcase == 'REJECTED' || self.app_control.state.upcase == 'FAILED'
|
190
205
|
end
|
191
206
|
|
192
207
|
end
|
data/lib/youtube_g/parser.rb
CHANGED
@@ -68,7 +68,10 @@ class YouTubeG
|
|
68
68
|
if app_control_element
|
69
69
|
app_control = YouTubeG::Model::Video::AppControl.new(
|
70
70
|
:draft => app_control_element.elements["app:draft"].text,
|
71
|
-
:state => app_control_element.elements["yt:state"].attributes["name"]
|
71
|
+
:state => app_control_element.elements["yt:state"].attributes["name"],
|
72
|
+
:reason => app_control_element.elements["yt:state"].attributes["reasonCode"],
|
73
|
+
:help_url => app_control_element.elements["yt:state"].attributes["helpUrl"],
|
74
|
+
:description => app_control_element.elements["yt:state"].text)
|
72
75
|
end
|
73
76
|
|
74
77
|
# parse the category and keyword lists
|
@@ -136,7 +139,12 @@ class YouTubeG
|
|
136
139
|
view_count = (el = entry.elements["yt:statistics"]) ? el.attributes["viewCount"].to_i : 0
|
137
140
|
|
138
141
|
noembed = entry.elements["yt:noembed"] ? true : false
|
139
|
-
racy = entry.elements["media:rating"] ? true : false
|
142
|
+
racy = entry.elements["media:rating"] ? true : false
|
143
|
+
|
144
|
+
status_url = nil
|
145
|
+
entry.elements.each("link") do |el|
|
146
|
+
status_url = el.attributes["href"] if el.attributes["rel"] == "self"
|
147
|
+
end
|
140
148
|
|
141
149
|
YouTubeG::Model::Video.new(
|
142
150
|
:video_id => video_id,
|
@@ -156,7 +164,8 @@ class YouTubeG
|
|
156
164
|
:rating => rating,
|
157
165
|
:view_count => view_count,
|
158
166
|
:noembed => noembed,
|
159
|
-
:racy => racy
|
167
|
+
:racy => racy,
|
168
|
+
:status_url => status_url)
|
160
169
|
end
|
161
170
|
|
162
171
|
def parse_media_content (media_content_element)
|
@@ -79,9 +79,13 @@ class YouTubeG
|
|
79
79
|
response = upload.post(direct_upload_url, upload_body, upload_header)
|
80
80
|
# todo parse this out also
|
81
81
|
if response.code.to_i == 403
|
82
|
+
logger.error("ERROR: #{response.code}")
|
82
83
|
raise AuthenticationError, response.body[/<TITLE>(.+)<\/TITLE>/, 1]
|
83
|
-
elsif response.code.to_i != 201
|
84
|
+
elsif response.code.to_i != 201
|
85
|
+
logger.error("ERROR: #{response.code}")
|
86
|
+
logger.debug("response: #{response.body}")
|
84
87
|
upload_errors = YouTubeG::Parser::UploadErrorParser.new(response.body).parse
|
88
|
+
logger.debug("upload_errors: #{upload_errors}")
|
85
89
|
raise UploadError, upload_errors.inspect
|
86
90
|
end
|
87
91
|
return YouTubeG::Parser::VideoFeedParser.new(response.body).parse
|
data/test/status.xml
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
2
|
+
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gml='http://www.opengis.net/gml'
|
3
|
+
xmlns:georss='http://www.georss.org/georss' xmlns:media='http://search.yahoo.com/mrss/'
|
4
|
+
xmlns:batch='http://schemas.google.com/gdata/batch' xmlns:yt='http://gdata.youtube.com/schemas/2007'
|
5
|
+
xmlns:gd='http://schemas.google.com/g/2005'>
|
6
|
+
<id>http://gdata.youtube.com/feeds/api/videos/QR1sWOVqxoM</id>
|
7
|
+
<published>2008-08-22T04:07:16.000-07:00</published>
|
8
|
+
<updated>2008-08-22T04:07:16.000-07:00</updated>
|
9
|
+
<app:control xmlns:app='http://purl.org/atom/app#'>
|
10
|
+
<app:draft>yes</app:draft>
|
11
|
+
<yt:state name='rejected'
|
12
|
+
reasonCode='tooLong'
|
13
|
+
helpUrl='http://www.youtube.com/t/community_guidelines'>Video is too long.</yt:state>
|
14
|
+
</app:control>
|
15
|
+
<category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='speechbox'/>
|
16
|
+
<category scheme='http://gdata.youtube.com/schemas/2007/categories.cat' term='Film' label='Film & Animation'/>
|
17
|
+
<category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#video'/>
|
18
|
+
<category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='wordia'/>
|
19
|
+
<title type='text'>discombobulate [08/22/2008 at 12:06PM]</title>
|
20
|
+
<content type='text'>To throw into a state of confusion; to befuddle or perplex</content>
|
21
|
+
<link rel='alternate' type='text/html' href='http://www.youtube.com/my_videos_edit?ns=1&video_id=QR1sWOVqxoM'/>
|
22
|
+
<link rel='http://gdata.youtube.com/schemas/2007#video.responses' type='application/atom+xml'
|
23
|
+
href='http://gdata.youtube.com/feeds/api/videos/QR1sWOVqxoM/responses'/>
|
24
|
+
<link rel='http://gdata.youtube.com/schemas/2007#video.related' type='application/atom+xml'
|
25
|
+
href='http://gdata.youtube.com/feeds/api/videos/QR1sWOVqxoM/related'/>
|
26
|
+
<link rel='self' type='application/atom+xml'
|
27
|
+
href='http://gdata.youtube.com/feeds/api/users/speechboxmatt/uploads/QR1sWOVqxoM'/>
|
28
|
+
<author>
|
29
|
+
<name>speechboxmatt</name>
|
30
|
+
<uri>http://gdata.youtube.com/feeds/api/users/speechboxmatt</uri>
|
31
|
+
</author>
|
32
|
+
<media:group>
|
33
|
+
<media:title type='plain'>discombobulate [08/22/2008 at 12:06PM]</media:title>
|
34
|
+
<media:description type='plain'>To throw into a state of confusion; to befuddle or perplex</media:description>
|
35
|
+
<media:keywords>speechbox, wordia</media:keywords>
|
36
|
+
<yt:duration seconds='0'/>
|
37
|
+
<media:category label='Film & Animation' scheme='http://gdata.youtube.com/schemas/2007/categories.cat'>
|
38
|
+
Film
|
39
|
+
</media:category>
|
40
|
+
<media:content url='http://www.youtube.com/v/QR1sWOVqxoM&f=gdata_user_uploads'
|
41
|
+
type='application/x-shockwave-flash' medium='video' isDefault='true' expression='full'
|
42
|
+
yt:format='5'/>
|
43
|
+
</media:group>
|
44
|
+
<gd:comments>
|
45
|
+
<gd:feedLink href='http://gdata.youtube.com/feeds/api/videos/QR1sWOVqxoM/comments' countHint='0'/>
|
46
|
+
</gd:comments>
|
47
|
+
</entry>
|
data/test/test_parser.rb
CHANGED
@@ -4,7 +4,8 @@ require File.dirname(__FILE__) + '/../lib/youtube_g'
|
|
4
4
|
class TestParser < Test::Unit::TestCase
|
5
5
|
|
6
6
|
UPLOAD_XML = File.new(File.dirname(__FILE__) +"/upload.xml")
|
7
|
-
SEARCH_XML = File.new(File.dirname(__FILE__) +"/search.xml")
|
7
|
+
SEARCH_XML = File.new(File.dirname(__FILE__) +"/search.xml")
|
8
|
+
YOUTUBE_STATUS_XML = File.new(File.dirname(__FILE__) +"/status.xml")
|
8
9
|
|
9
10
|
YT_VALIDATION = "yt:validation"
|
10
11
|
REQUIRED = "required"
|
@@ -18,6 +19,10 @@ class TestParser < Test::Unit::TestCase
|
|
18
19
|
@vf_parser = YouTubeG::Parser::VideoFeedParser.new(UPLOAD_XML.path)
|
19
20
|
@vf_parser_result = @vf_parser.parse
|
20
21
|
|
22
|
+
# video feed error
|
23
|
+
@vfe_parser = YouTubeG::Parser::VideoFeedParser.new(YOUTUBE_STATUS_XML.path)
|
24
|
+
@vfe_parser_result = @vfe_parser.parse
|
25
|
+
|
21
26
|
@vfs_parser = YouTubeG::Parser::VideosFeedParser.new(SEARCH_XML.path)
|
22
27
|
@vfs_parser_result = @vfs_parser.parse
|
23
28
|
end
|
@@ -93,7 +98,9 @@ class TestParser < Test::Unit::TestCase
|
|
93
98
|
assert_equal(320,video.thumbnails[3].width)
|
94
99
|
assert_equal("00:00:07.500",video.thumbnails[3].time)
|
95
100
|
|
96
|
-
assert_equal("http://www.youtube.com/watch?v=32_mQ0PRT9I",video.player_url)
|
101
|
+
assert_equal("http://www.youtube.com/watch?v=32_mQ0PRT9I",video.player_url)
|
102
|
+
|
103
|
+
assert_equal("http://gdata.youtube.com/feeds/api/users/speechboxmatt/uploads/32_mQ0PRT9I",video.status_url);
|
97
104
|
end
|
98
105
|
end
|
99
106
|
|
@@ -143,6 +150,21 @@ class TestParser < Test::Unit::TestCase
|
|
143
150
|
assert_equal("application/x-shockwave-flash",video.media_content[0].mime_type)
|
144
151
|
end
|
145
152
|
|
153
|
+
def test_video_feed_parser_with_error_status
|
154
|
+
video = @vfe_parser_result
|
155
|
+
assert_instance_of(YouTubeG::Model::Video, video)
|
156
|
+
|
157
|
+
assert_equal("http://gdata.youtube.com/feeds/api/videos/QR1sWOVqxoM",video.video_id)
|
158
|
+
|
159
|
+
# App control - state of the upload
|
160
|
+
assert_equal("yes", video.app_control.draft)
|
161
|
+
assert_equal("rejected", video.app_control.state)
|
162
|
+
# these populated only if errored
|
163
|
+
assert_equal("tooLong", video.app_control.reason)
|
164
|
+
assert_equal("http://www.youtube.com/t/community_guidelines", video.app_control.help_url)
|
165
|
+
assert_equal("Video is too long.", video.app_control.description)
|
166
|
+
end
|
167
|
+
|
146
168
|
def well_formed_errors_xml
|
147
169
|
errors_xml = ''
|
148
170
|
errors_xml << "<?xml version='1.0' encoding='UTF-8'?>"
|
@@ -169,4 +191,4 @@ class TestParser < Test::Unit::TestCase
|
|
169
191
|
errors_xml << " <location type='xpath'>media:group/media:title/text()</location>"
|
170
192
|
errors_xml << "</errors>"
|
171
193
|
end
|
172
|
-
end
|
194
|
+
end
|
data/test/test_video.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/youtube_g'
|
3
|
+
|
4
|
+
class TestVideo < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_processed_method_for_nil_state
|
7
|
+
# we assume no app_control from YT means processed.
|
8
|
+
video = YouTubeG::Model::Video.new({})
|
9
|
+
assert_equal(true, video.processed?)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_processed_method_for_processing_state
|
13
|
+
video = YouTubeG::Model::Video.new({:app_control => YouTubeG::Model::Video::AppControl.new({:state => 'processing'})})
|
14
|
+
assert_equal(false, video.processed?)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_processed_method_for_rejected_state
|
18
|
+
video = YouTubeG::Model::Video.new({:app_control => YouTubeG::Model::Video::AppControl.new({:state => 'rejected'})})
|
19
|
+
assert_equal(false, video.processed?)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_processed_method_for_failed_state
|
23
|
+
video = YouTubeG::Model::Video.new({:app_control => YouTubeG::Model::Video::AppControl.new({:state => 'failed'})})
|
24
|
+
assert_equal(false, video.processed?)
|
25
|
+
end
|
26
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: msp-youtube-g
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.8.
|
4
|
+
version: 0.4.8.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shane Vitarana
|
@@ -58,9 +58,11 @@ files:
|
|
58
58
|
- integration-test/test_video.rb
|
59
59
|
- integration-test/test_video_search.rb
|
60
60
|
- test/test_parser.rb
|
61
|
+
- test/test_video.rb
|
61
62
|
- test/test_upload.rb
|
62
63
|
- test/search.xml
|
63
64
|
- test/upload.xml
|
65
|
+
- test/status.xml
|
64
66
|
has_rdoc: true
|
65
67
|
homepage: http://youtube-g.rubyforge.org/
|
66
68
|
post_install_message:
|