tubeclip 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/tubeclip/chain_io.rb +86 -0
- data/lib/tubeclip/client.rb +541 -0
- data/lib/tubeclip/middleware/faraday_authheader.rb +24 -0
- data/lib/tubeclip/middleware/faraday_oauth.rb +21 -0
- data/lib/tubeclip/middleware/faraday_oauth2.rb +13 -0
- data/lib/tubeclip/middleware/faraday_tubeclip.rb +38 -0
- data/lib/tubeclip/model/activity.rb +17 -0
- data/lib/tubeclip/model/author.rb +13 -0
- data/lib/tubeclip/model/caption.rb +7 -0
- data/lib/tubeclip/model/category.rb +11 -0
- data/lib/tubeclip/model/comment.rb +18 -0
- data/lib/tubeclip/model/contact.rb +19 -0
- data/lib/tubeclip/model/content.rb +18 -0
- data/lib/tubeclip/model/message.rb +12 -0
- data/lib/tubeclip/model/playlist.rb +11 -0
- data/lib/tubeclip/model/rating.rb +23 -0
- data/lib/tubeclip/model/subscription.rb +7 -0
- data/lib/tubeclip/model/thumbnail.rb +20 -0
- data/lib/tubeclip/model/user.rb +35 -0
- data/lib/tubeclip/model/video.rb +302 -0
- data/lib/tubeclip/parser.rb +643 -0
- data/lib/tubeclip/record.rb +12 -0
- data/lib/tubeclip/request/base_search.rb +76 -0
- data/lib/tubeclip/request/error.rb +21 -0
- data/lib/tubeclip/request/remote_file.rb +70 -0
- data/lib/tubeclip/request/standard_search.rb +49 -0
- data/lib/tubeclip/request/user_search.rb +47 -0
- data/lib/tubeclip/request/video_search.rb +125 -0
- data/lib/tubeclip/request/video_upload.rb +762 -0
- data/lib/tubeclip/response/video_search.rb +41 -0
- data/lib/tubeclip/version.rb +3 -0
- data/lib/tubeclip.rb +85 -0
- data/tubeclip.gemspec +44 -0
- metadata +259 -0
@@ -0,0 +1,643 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class Tubeclip
|
4
|
+
module Parser #:nodoc:
|
5
|
+
class FeedParser #:nodoc:
|
6
|
+
def initialize(content)
|
7
|
+
@content = (content =~ URI::regexp(%w(http https)) ? open(content).read : content)
|
8
|
+
|
9
|
+
rescue OpenURI::HTTPError => e
|
10
|
+
raise OpenURI::HTTPError.new(e.io.status[0],e)
|
11
|
+
rescue
|
12
|
+
@content = content
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
parse_content @content
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_single_entry
|
21
|
+
doc = Nokogiri::XML(@content)
|
22
|
+
parse_entry(doc.at("entry") || doc)
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_videos
|
26
|
+
doc = Nokogiri::XML(@content)
|
27
|
+
videos = []
|
28
|
+
doc.css("entry").each do |video|
|
29
|
+
videos << parse_entry(video)
|
30
|
+
end
|
31
|
+
videos
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove_bom str
|
35
|
+
str.gsub /\xEF\xBB\xBF|/, '' if str
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class CommentsFeedParser < FeedParser #:nodoc:
|
40
|
+
# return array of comments
|
41
|
+
def parse_content(content)
|
42
|
+
doc = Nokogiri::XML(content.body)
|
43
|
+
feed = doc.at("feed")
|
44
|
+
|
45
|
+
comments = []
|
46
|
+
feed.css("entry").each do |entry|
|
47
|
+
comments << parse_entry(entry)
|
48
|
+
end
|
49
|
+
return comments
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
def parse_entry(entry)
|
54
|
+
author = Tubeclip::Model::Author.new(
|
55
|
+
:name => (entry.at("author/name").text rescue nil),
|
56
|
+
:uri => (entry.at("author/uri").text rescue nil)
|
57
|
+
)
|
58
|
+
Tubeclip::Model::Comment.new(
|
59
|
+
:author => author,
|
60
|
+
:content => remove_bom(entry.at("content").text),
|
61
|
+
:published => entry.at("published").text,
|
62
|
+
:title => remove_bom(entry.at("title").text),
|
63
|
+
:updated => entry.at("updated").text,
|
64
|
+
:url => entry.at("id").text,
|
65
|
+
:reply_to => parse_reply(entry),
|
66
|
+
:channel_id => (entry.at("yt|channelId").text rescue nil),
|
67
|
+
:gp_user_id => (entry.at("yt|googlePlusUserId").text rescue nil)
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_reply(entry)
|
72
|
+
if link = entry.at_xpath("xmlns:link[@rel='http://gdata.youtube.com/schemas/2007#in-reply-to']")
|
73
|
+
link["href"].split('/').last.gsub(/\?client.*/, '')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class PlaylistFeedParser < FeedParser #:nodoc:
|
79
|
+
|
80
|
+
def parse_content(content)
|
81
|
+
xml = Nokogiri::XML(content.body)
|
82
|
+
entry = xml.at("feed") || xml.at("entry")
|
83
|
+
Tubeclip::Model::Playlist.new(
|
84
|
+
:title => entry.at("title") && entry.at("title").text,
|
85
|
+
:summary => ((entry.at("summary") || entry.at_xpath("media:group").at_xpath("media:description")).text rescue nil),
|
86
|
+
:description => ((entry.at("summary") || entry.at_xpath("media:group").at_xpath("media:description")).text rescue nil),
|
87
|
+
:author => (entry.at("author name").text rescue nil),
|
88
|
+
:playlist_id => (entry.at("id").text[/playlist:([\w\-]+)/, 1] rescue nil),
|
89
|
+
:published => entry.at("published") ? entry.at("published").text : nil,
|
90
|
+
:videos_count => (entry.at_xpath("openSearch:totalResults").text rescue nil),
|
91
|
+
:response_code => content.status,
|
92
|
+
:xml => content.body)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class PlaylistsFeedParser < FeedParser #:nodoc:
|
97
|
+
|
98
|
+
# return array of playlist objects
|
99
|
+
def parse_content(content)
|
100
|
+
doc = Nokogiri::XML(content.body)
|
101
|
+
feed = doc.at("feed")
|
102
|
+
|
103
|
+
playlists = []
|
104
|
+
feed.css("entry").each do |entry|
|
105
|
+
playlists << parse_entry(entry)
|
106
|
+
end
|
107
|
+
return playlists
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
def parse_entry(entry)
|
113
|
+
Tubeclip::Model::Playlist.new(
|
114
|
+
:title => entry.at("title").text,
|
115
|
+
:summary => (entry.at("summary") || entry.at_xpath("media:group").at_xpath("media:description")).text,
|
116
|
+
:description => (entry.at("summary") || entry.at_xpath("media:group").at_xpath("media:description")).text,
|
117
|
+
:playlist_id => entry.at("id").text[/playlist([^<]+)/, 1].sub(':',''),
|
118
|
+
:published => entry.at("published") ? entry.at("published").text : nil,
|
119
|
+
:response_code => nil,
|
120
|
+
:xml => nil)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns an array of the user's activity
|
125
|
+
class ActivityParser < FeedParser
|
126
|
+
def parse_content(content)
|
127
|
+
doc = Nokogiri::XML(content.body)
|
128
|
+
feed = doc.at("feed")
|
129
|
+
|
130
|
+
activities = []
|
131
|
+
feed.css("entry").each do |entry|
|
132
|
+
if parsed_activity = parse_activity(entry)
|
133
|
+
activities << parsed_activity
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
return activities
|
138
|
+
end
|
139
|
+
|
140
|
+
protected
|
141
|
+
|
142
|
+
# Parses the user's activity feed.
|
143
|
+
def parse_activity(entry)
|
144
|
+
# Figure out what kind of activity we have
|
145
|
+
video_type = nil
|
146
|
+
parsed_activity = nil
|
147
|
+
entry.css("category").each do |category_tag|
|
148
|
+
if category_tag["scheme"] == "http://gdata.youtube.com/schemas/2007/userevents.cat"
|
149
|
+
video_type = category_tag["term"]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if video_type
|
154
|
+
case video_type
|
155
|
+
when "video_rated"
|
156
|
+
parsed_activity = Tubeclip::Model::Activity.new(
|
157
|
+
:type => "video_rated",
|
158
|
+
:time => entry.at("updated") ? entry.at("updated").text : nil,
|
159
|
+
:author => entry.at("author/name") ? entry.at("author/name").text : nil,
|
160
|
+
:videos => parse_activity_videos(entry),
|
161
|
+
:video_id => entry.at_xpath("yt:videoid") ? entry.at_xpath("yt:videoid").text : nil
|
162
|
+
)
|
163
|
+
when "video_shared"
|
164
|
+
parsed_activity = Tubeclip::Model::Activity.new(
|
165
|
+
:type => "video_shared",
|
166
|
+
:time => entry.at("updated") ? entry.at("updated").text : nil,
|
167
|
+
:author => entry.at("author/name") ? entry.at("author/name").text : nil,
|
168
|
+
:videos => parse_activity_videos(entry),
|
169
|
+
:video_id => entry.at_xpath("yt:videoid") ? entry.at_xpath("yt:videoid").text : nil
|
170
|
+
)
|
171
|
+
when "video_favorited"
|
172
|
+
parsed_activity = Tubeclip::Model::Activity.new(
|
173
|
+
:type => "video_favorited",
|
174
|
+
:time => entry.at("updated") ? entry.at("updated").text : nil,
|
175
|
+
:author => entry.at("author/name") ? entry.at("author/name").text : nil,
|
176
|
+
:videos => parse_activity_videos(entry),
|
177
|
+
:video_id => entry.at_xpath("yt:videoid") ? entry.at_xpath("yt:videoid").text : nil
|
178
|
+
)
|
179
|
+
when "video_commented"
|
180
|
+
# Load the comment and video URL
|
181
|
+
comment_thread_url = nil
|
182
|
+
video_url = nil
|
183
|
+
entry.css("link").each do |link_tag|
|
184
|
+
case link_tag["rel"]
|
185
|
+
when "http://gdata.youtube.com/schemas/2007#comments"
|
186
|
+
comment_thread_url = link_tag["href"]
|
187
|
+
when "http://gdata.youtube.com/schemas/2007#video"
|
188
|
+
video_url = link_tag["href"]
|
189
|
+
else
|
190
|
+
# Invalid rel type, do nothing
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
parsed_activity = Tubeclip::Model::Activity.new(
|
195
|
+
:type => "video_commented",
|
196
|
+
:time => entry.at("updated") ? entry.at("updated").text : nil,
|
197
|
+
:author => entry.at("author/name") ? entry.at("author/name").text : nil,
|
198
|
+
:videos => parse_activity_videos(entry),
|
199
|
+
:video_id => entry.at_xpath("yt:videoid") ? entry.at_xpath("yt:videoid").text : nil,
|
200
|
+
:comment_thread_url => comment_thread_url,
|
201
|
+
:video_url => video_url
|
202
|
+
)
|
203
|
+
when "video_uploaded"
|
204
|
+
parsed_activity = Tubeclip::Model::Activity.new(
|
205
|
+
:type => "video_uploaded",
|
206
|
+
:time => entry.at("updated") ? entry.at("updated").text : nil,
|
207
|
+
:author => entry.at("author/name") ? entry.at("author/name").text : nil,
|
208
|
+
:videos => parse_activity_videos(entry),
|
209
|
+
:video_id => entry.at_xpath("yt:videoid") ? entry.at_xpath("yt:videoid").text : nil
|
210
|
+
)
|
211
|
+
when "friend_added"
|
212
|
+
parsed_activity = Tubeclip::Model::Activity.new(
|
213
|
+
:type => "friend_added",
|
214
|
+
:time => entry.at("updated") ? entry.at("updated").text : nil,
|
215
|
+
:author => entry.at("author/name") ? entry.at("author/name").text : nil,
|
216
|
+
:username => entry.at_xpath("yt:username") ? entry.at_xpath("yt:username").text : nil
|
217
|
+
)
|
218
|
+
when "user_subscription_added"
|
219
|
+
parsed_activity = Tubeclip::Model::Activity.new(
|
220
|
+
:type => "user_subscription_added",
|
221
|
+
:time => entry.at("updated") ? entry.at("updated").text : nil,
|
222
|
+
:author => entry.at("author/name") ? entry.at("author/name").text : nil,
|
223
|
+
:username => entry.at_xpath("yt:username") ? entry.at_xpath("yt:username").text : nil
|
224
|
+
)
|
225
|
+
else
|
226
|
+
# Invalid activity type, just let it return nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
return parsed_activity
|
231
|
+
end
|
232
|
+
|
233
|
+
# If a user enabled inline attribute videos may be included in results.
|
234
|
+
def parse_activity_videos(entry)
|
235
|
+
videos = []
|
236
|
+
|
237
|
+
entry.css("link").each do |link_tag|
|
238
|
+
videos << Tubeclip::Parser::VideoFeedParser.new(link_tag).parse if link_tag.at("entry")
|
239
|
+
end
|
240
|
+
|
241
|
+
if videos.size <= 0
|
242
|
+
videos = nil
|
243
|
+
end
|
244
|
+
|
245
|
+
return videos
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Returns an array of the user's contacts
|
250
|
+
class ContactsParser < FeedParser
|
251
|
+
def parse_content(content)
|
252
|
+
doc = Nokogiri::XML(content.body)
|
253
|
+
feed = doc.at("feed")
|
254
|
+
|
255
|
+
contacts = []
|
256
|
+
feed.css("entry").each do |entry|
|
257
|
+
temp_contact = Tubeclip::Model::Contact.new(
|
258
|
+
:title => entry.at("title") ? entry.at("title").text : nil,
|
259
|
+
:username => entry.at_xpath("yt:username") ? entry.at_xpath("yt:username").text : nil,
|
260
|
+
:status => entry.at_xpath("yt:status") ? entry.at_xpath("yt:status").text : nil
|
261
|
+
)
|
262
|
+
|
263
|
+
contacts << temp_contact
|
264
|
+
end
|
265
|
+
|
266
|
+
return contacts
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Returns an array of the user's messages
|
271
|
+
class MessagesParser < FeedParser
|
272
|
+
def parse_content(content)
|
273
|
+
doc = Nokogiri::XML(content.body)
|
274
|
+
feed = doc.at("feed")
|
275
|
+
|
276
|
+
messages = []
|
277
|
+
feed.css("entry").each do |entry|
|
278
|
+
author = entry.at("author")
|
279
|
+
temp_message = Tubeclip::Model::Message.new(
|
280
|
+
:id => entry.at("id") ? entry.at("id").text.gsub(/.+:inbox:/, "") : nil,
|
281
|
+
:title => entry.at("title") ? entry.at("title").text : nil,
|
282
|
+
:name => author && author.at("name") ? author.at("name").text : nil,
|
283
|
+
:summary => entry.at("summary") ? entry.at("summary").text : nil,
|
284
|
+
:published => entry.at("published") ? entry.at("published").text : nil
|
285
|
+
)
|
286
|
+
|
287
|
+
messages << temp_message
|
288
|
+
end
|
289
|
+
|
290
|
+
return messages
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class ProfileFeedParser < FeedParser #:nodoc:
|
295
|
+
def parse_content(content)
|
296
|
+
xml = Nokogiri::XML(content.body)
|
297
|
+
entry = xml.at("entry") || xml.at("feed")
|
298
|
+
parse_entry(entry)
|
299
|
+
end
|
300
|
+
def parse_entry(entry)
|
301
|
+
Tubeclip::Model::User.new(
|
302
|
+
:age => entry.at_xpath("yt:age") ? entry.at_xpath("yt:age").text : nil,
|
303
|
+
:username => entry.at_xpath("yt:username") ? entry.at_xpath("yt:username").text : nil,
|
304
|
+
:username_display => (entry.at_xpath("yt:username")['display'] rescue nil),
|
305
|
+
:user_id => (entry.at_xpath("xmlns:author/yt:userId").text rescue nil),
|
306
|
+
:last_name => (entry.at_xpath("yt:lastName").text rescue nil),
|
307
|
+
:first_name => (entry.at_xpath("yt:firstName").text rescue nil),
|
308
|
+
:company => entry.at_xpath("yt:company") ? entry.at_xpath("yt:company").text : nil,
|
309
|
+
:gender => entry.at_xpath("yt:gender") ? entry.at_xpath("yt:gender").text : nil,
|
310
|
+
:hobbies => entry.at_xpath("yt:hobbies") ? entry.at_xpath("yt:hobbies").text : nil,
|
311
|
+
:hometown => entry.at_xpath("yt:hometown") ? entry.at_xpath("yt:hometown").text : nil,
|
312
|
+
:location => entry.at_xpath("yt:location") ? entry.at_xpath("yt:location").text : nil,
|
313
|
+
:last_login => entry.at_xpath("yt:statistics")["lastWebAccess"],
|
314
|
+
:join_date => entry.at("published") ? entry.at("published").text : nil,
|
315
|
+
:movies => entry.at_xpath("yt:movies") ? entry.at_xpath("yt:movies").text : nil,
|
316
|
+
:music => entry.at_xpath("yt:music") ? entry.at_xpath("yt:music").text : nil,
|
317
|
+
:occupation => entry.at_xpath("yt:occupation") ? entry.at_xpath("yt:occupation").text : nil,
|
318
|
+
:relationship => entry.at_xpath("yt:relationship") ? entry.at_xpath("yt:relationship").text : nil,
|
319
|
+
:school => entry.at_xpath("yt:school") ? entry.at_xpath("yt:school").text : nil,
|
320
|
+
:avatar => entry.at_xpath("media:thumbnail") ? entry.at_xpath("media:thumbnail")["url"] : nil,
|
321
|
+
:upload_count => (entry.at_xpath('gd:feedLink[@rel="http://gdata.youtube.com/schemas/2007#user.uploads"]')['countHint'].to_i rescue nil),
|
322
|
+
:max_upload_duration => (entry.at_xpath("yt:maxUploadDuration")['seconds'].to_i rescue nil),
|
323
|
+
:subscribers => entry.at_xpath("yt:statistics")["subscriberCount"],
|
324
|
+
:videos_watched => entry.at_xpath("yt:statistics")["videoWatchCount"],
|
325
|
+
:view_count => entry.at_xpath("yt:statistics")["viewCount"],
|
326
|
+
:upload_views => entry.at_xpath("yt:statistics")["totalUploadViews"],
|
327
|
+
:insight_uri => (entry.at_xpath('xmlns:link[@rel="http://gdata.youtube.com/schemas/2007#insight.views"]')['href'] rescue nil),
|
328
|
+
:channel_uri => (entry.at_xpath('xmlns:link[@rel="alternate"]')['href'] rescue nil),
|
329
|
+
)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
class BatchProfileFeedParser < ProfileFeedParser
|
334
|
+
def parse_content(content)
|
335
|
+
Nokogiri::XML(content.body).xpath("//xmlns:entry").map do |entry|
|
336
|
+
entry.namespaces.each {|name, url| entry.document.root.add_namespace name, url }
|
337
|
+
username = entry.at_xpath('batch:id', entry.namespaces).text
|
338
|
+
result = catch(:result) do
|
339
|
+
case entry.at_xpath('batch:status', entry.namespaces)['code'].to_i
|
340
|
+
when 200...300 then parse_entry(entry)
|
341
|
+
else nil
|
342
|
+
end
|
343
|
+
end
|
344
|
+
{ username => result }
|
345
|
+
end.reduce({},:merge)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
class SubscriptionFeedParser < FeedParser #:nodoc:
|
350
|
+
|
351
|
+
def parse_content(content)
|
352
|
+
doc = Nokogiri::XML(content.body)
|
353
|
+
feed = doc.at("feed")
|
354
|
+
|
355
|
+
subscriptions = []
|
356
|
+
feed.css("entry").each do |entry|
|
357
|
+
subscriptions << parse_entry(entry)
|
358
|
+
end
|
359
|
+
return subscriptions
|
360
|
+
end
|
361
|
+
|
362
|
+
protected
|
363
|
+
|
364
|
+
def parse_entry(entry)
|
365
|
+
Tubeclip::Model::Subscription.new(
|
366
|
+
:title => entry.at("title").text,
|
367
|
+
:id => entry.at("id").text[/subscription([^<]+)/, 1].sub(':',''),
|
368
|
+
:published => entry.at("published") ? entry.at("published").text : nil,
|
369
|
+
:youtube_user_name => entry.to_s.split(/\<|\>/)[-4]
|
370
|
+
)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
class CaptionFeedParser < FeedParser #:nodoc:
|
375
|
+
|
376
|
+
def parse_content(content)
|
377
|
+
doc = (content.is_a?(Nokogiri::XML::Document)) ? content : Nokogiri::XML(content)
|
378
|
+
|
379
|
+
entry = doc.at "entry"
|
380
|
+
parse_entry(entry)
|
381
|
+
end
|
382
|
+
|
383
|
+
protected
|
384
|
+
|
385
|
+
def parse_entry(entry)
|
386
|
+
Tubeclip::Model::Caption.new(
|
387
|
+
:title => entry.at("title").text,
|
388
|
+
:id => entry.at("id").text[/captions([^<]+)/, 1].sub(':',''),
|
389
|
+
:published => entry.at("published") ? entry.at("published").text : nil
|
390
|
+
)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
class VideoFeedParser < FeedParser #:nodoc:
|
395
|
+
|
396
|
+
def parse_content(content)
|
397
|
+
doc = (content.is_a?(Nokogiri::XML::Document)) ? content : Nokogiri::XML(content)
|
398
|
+
|
399
|
+
entry = doc.at "entry"
|
400
|
+
parse_entry(entry)
|
401
|
+
end
|
402
|
+
|
403
|
+
protected
|
404
|
+
def parse_entry(entry)
|
405
|
+
video_id = entry.at("id").text
|
406
|
+
published_at = entry.at("published") ? Time.parse(entry.at("published").text) : nil
|
407
|
+
uploaded_at = entry.at_xpath("media:group/yt:uploaded") ? Time.parse(entry.at_xpath("media:group/yt:uploaded").text) : nil
|
408
|
+
updated_at = entry.at("updated") ? Time.parse(entry.at("updated").text) : nil
|
409
|
+
recorded_at = entry.at_xpath("yt:recorded") ? Time.parse(entry.at_xpath("yt:recorded").text) : nil
|
410
|
+
|
411
|
+
# parse the category and keyword lists
|
412
|
+
categories = []
|
413
|
+
keywords = []
|
414
|
+
entry.css("category").each do |category|
|
415
|
+
# determine if it's really a category, or just a keyword
|
416
|
+
scheme = category["scheme"]
|
417
|
+
if (scheme =~ /\/categories\.cat$/)
|
418
|
+
# it's a category
|
419
|
+
categories << Tubeclip::Model::Category.new(
|
420
|
+
:term => category["term"],
|
421
|
+
:label => category["label"])
|
422
|
+
|
423
|
+
elsif (scheme =~ /\/keywords\.cat$/)
|
424
|
+
# it's a keyword
|
425
|
+
keywords << category["term"]
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
title = entry.at("title").text
|
430
|
+
html_content = nil #entry.at("content") ? entry.at("content").text : nil
|
431
|
+
|
432
|
+
# parse the author
|
433
|
+
author_element = entry.at("author")
|
434
|
+
author = nil
|
435
|
+
if author_element
|
436
|
+
author = Tubeclip::Model::Author.new(
|
437
|
+
:name => author_element.at("name").text,
|
438
|
+
:uri => author_element.at("uri").text)
|
439
|
+
end
|
440
|
+
media_group = entry.at_xpath('media:group')
|
441
|
+
|
442
|
+
ytid = nil
|
443
|
+
unless media_group.at_xpath("yt:videoid").nil?
|
444
|
+
ytid = media_group.at_xpath("yt:videoid").text
|
445
|
+
end
|
446
|
+
|
447
|
+
# if content is not available on certain region, there is no media:description, media:player or yt:duration
|
448
|
+
description = ""
|
449
|
+
unless media_group.at_xpath("media:description").nil?
|
450
|
+
description = media_group.at_xpath("media:description").text
|
451
|
+
end
|
452
|
+
|
453
|
+
# if content is not available on certain region, there is no media:description, media:player or yt:duration
|
454
|
+
duration = 0
|
455
|
+
unless media_group.at_xpath("yt:duration").nil?
|
456
|
+
duration = media_group.at_xpath("yt:duration")["seconds"].to_i
|
457
|
+
end
|
458
|
+
|
459
|
+
# if content is not available on certain region, there is no media:description, media:player or yt:duration
|
460
|
+
player_url = ""
|
461
|
+
unless media_group.at_xpath("media:player").nil?
|
462
|
+
player_url = media_group.at_xpath("media:player")["url"]
|
463
|
+
end
|
464
|
+
|
465
|
+
unless media_group.at_xpath("yt:aspectRatio").nil?
|
466
|
+
widescreen = media_group.at_xpath("yt:aspectRatio").text == 'widescreen' ? true : false
|
467
|
+
end
|
468
|
+
|
469
|
+
media_content = []
|
470
|
+
media_group.xpath("media:content").each do |mce|
|
471
|
+
media_content << parse_media_content(mce)
|
472
|
+
end
|
473
|
+
|
474
|
+
# parse thumbnails
|
475
|
+
thumbnails = []
|
476
|
+
media_group.xpath("media:thumbnail").each do |thumb_element|
|
477
|
+
# TODO: convert time HH:MM:ss string to seconds?
|
478
|
+
thumbnails << Tubeclip::Model::Thumbnail.new(
|
479
|
+
:url => thumb_element["url"],
|
480
|
+
:height => thumb_element["height"].to_i,
|
481
|
+
:width => thumb_element["width"].to_i,
|
482
|
+
:time => thumb_element["time"],
|
483
|
+
:name => thumb_element["yt:name"])
|
484
|
+
end
|
485
|
+
|
486
|
+
rating_element = entry.at_xpath("gd:rating") rescue nil
|
487
|
+
extended_rating_element = entry.at_xpath("yt:rating") rescue nil
|
488
|
+
unless entry.at_xpath("yt:position").nil?
|
489
|
+
video_position = entry.at_xpath("yt:position").text
|
490
|
+
end
|
491
|
+
|
492
|
+
rating = nil
|
493
|
+
if rating_element
|
494
|
+
rating_values = {
|
495
|
+
:min => rating_element["min"].to_i,
|
496
|
+
:max => rating_element["max"].to_i,
|
497
|
+
:rater_count => rating_element["numRaters"].to_i,
|
498
|
+
:average => rating_element["average"].to_f
|
499
|
+
}
|
500
|
+
|
501
|
+
if extended_rating_element
|
502
|
+
rating_values[:likes] = extended_rating_element["numLikes"].to_i
|
503
|
+
rating_values[:dislikes] = extended_rating_element["numDislikes"].to_i
|
504
|
+
end
|
505
|
+
|
506
|
+
rating = Tubeclip::Model::Rating.new(rating_values)
|
507
|
+
end
|
508
|
+
|
509
|
+
if (el = entry.at_xpath("yt:statistics"))
|
510
|
+
view_count, favorite_count = el["viewCount"].to_i, el["favoriteCount"].to_i
|
511
|
+
else
|
512
|
+
view_count, favorite_count = 0,0
|
513
|
+
end
|
514
|
+
|
515
|
+
comment_feed = entry.at_xpath('gd:comments/gd:feedLink[@rel="http://gdata.youtube.com/schemas/2007#comments"]') rescue nil
|
516
|
+
comment_count = comment_feed ? comment_feed['countHint'].to_i : 0
|
517
|
+
|
518
|
+
access_control = entry.xpath('yt:accessControl').map do |e|
|
519
|
+
{ e['action'] => e['permission'] }
|
520
|
+
end.compact.reduce({},:merge)
|
521
|
+
|
522
|
+
noembed = entry.at_xpath("yt:noembed") ? true : false
|
523
|
+
safe_search = entry.at_xpath("media:rating") ? true : false
|
524
|
+
|
525
|
+
if entry.namespaces['xmlns:georss'] and where = entry.at_xpath("georss:where")
|
526
|
+
position = where.at_xpath("gml:Point").at_xpath("gml:pos").text
|
527
|
+
latitude, longitude = position.split.map &:to_f
|
528
|
+
end
|
529
|
+
|
530
|
+
if entry.namespaces['xmlns:app']
|
531
|
+
control = entry.at_xpath("app:control")
|
532
|
+
state = { :name => "published" }
|
533
|
+
if control && control.at_xpath("yt:state")
|
534
|
+
state = {
|
535
|
+
:name => control.at_xpath("yt:state")["name"],
|
536
|
+
:reason_code => control.at_xpath("yt:state")["reasonCode"],
|
537
|
+
:help_url => control.at_xpath("yt:state")["helpUrl"],
|
538
|
+
:copy => control.at_xpath("yt:state").text
|
539
|
+
}
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
insight_uri = (entry.at_xpath('xmlns:link[@rel="http://gdata.youtube.com/schemas/2007#insight.views"]')['href'] rescue nil)
|
544
|
+
|
545
|
+
perm_private = media_group.at_xpath("yt:private") ? true : false
|
546
|
+
|
547
|
+
Tubeclip::Model::Video.new(
|
548
|
+
:video_id => video_id,
|
549
|
+
:published_at => published_at,
|
550
|
+
:updated_at => updated_at,
|
551
|
+
:uploaded_at => uploaded_at,
|
552
|
+
:recorded_at => recorded_at,
|
553
|
+
:categories => categories,
|
554
|
+
:keywords => keywords,
|
555
|
+
:title => title,
|
556
|
+
:author => author,
|
557
|
+
:description => description,
|
558
|
+
:duration => duration,
|
559
|
+
:media_content => media_content,
|
560
|
+
:player_url => player_url,
|
561
|
+
:thumbnails => thumbnails,
|
562
|
+
:rating => rating,
|
563
|
+
:view_count => view_count,
|
564
|
+
:favorite_count => favorite_count,
|
565
|
+
:comment_count => comment_count,
|
566
|
+
:access_control => access_control,
|
567
|
+
:widescreen => widescreen,
|
568
|
+
:noembed => noembed,
|
569
|
+
:safe_search => safe_search,
|
570
|
+
:position => position,
|
571
|
+
:video_position => video_position,
|
572
|
+
:latitude => latitude,
|
573
|
+
:longitude => longitude,
|
574
|
+
:state => state,
|
575
|
+
:insight_uri => insight_uri,
|
576
|
+
:unique_id => ytid,
|
577
|
+
:raw_content => entry,
|
578
|
+
:perm_private => perm_private)
|
579
|
+
end
|
580
|
+
|
581
|
+
def parse_media_content (elem)
|
582
|
+
content_url = elem["url"]
|
583
|
+
format_code = elem["yt:format"].to_i
|
584
|
+
format = Tubeclip::Model::Video::Format.by_code(format_code)
|
585
|
+
duration = elem["duration"].to_i
|
586
|
+
mime_type = elem["type"]
|
587
|
+
default = (elem["isDefault"] == "true")
|
588
|
+
|
589
|
+
Tubeclip::Model::Content.new(
|
590
|
+
:url => content_url,
|
591
|
+
:format => format,
|
592
|
+
:duration => duration,
|
593
|
+
:mime_type => mime_type,
|
594
|
+
:default => default)
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
class BatchVideoFeedParser < VideoFeedParser
|
599
|
+
def parse_content(content)
|
600
|
+
Nokogiri::XML(content.body).xpath("//xmlns:entry").map do |entry|
|
601
|
+
entry.namespaces.each {|name, url| entry.document.root.add_namespace name, url }
|
602
|
+
username = entry.at_xpath('batch:id', entry.namespaces).text
|
603
|
+
result = catch(:result) do
|
604
|
+
case entry.at_xpath('batch:status', entry.namespaces)['code'].to_i
|
605
|
+
when 200...300 then parse_entry(entry)
|
606
|
+
else nil
|
607
|
+
end
|
608
|
+
end
|
609
|
+
{ username => result }
|
610
|
+
end.reduce({},:merge)
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
class VideosFeedParser < VideoFeedParser #:nodoc:
|
615
|
+
|
616
|
+
private
|
617
|
+
def parse_content(content)
|
618
|
+
videos = []
|
619
|
+
doc = Nokogiri::XML(content)
|
620
|
+
feed = doc.at "feed"
|
621
|
+
if feed
|
622
|
+
feed_id = feed.at("id").text
|
623
|
+
updated_at = Time.parse(feed.at("updated").text)
|
624
|
+
total_result_count = feed.at_xpath("openSearch:totalResults").text.to_i
|
625
|
+
offset = feed.at_xpath("openSearch:startIndex").text.to_i
|
626
|
+
max_result_count = feed.at_xpath("openSearch:itemsPerPage").text.to_i
|
627
|
+
|
628
|
+
feed.css("entry").each do |entry|
|
629
|
+
videos << parse_entry(entry)
|
630
|
+
end
|
631
|
+
end
|
632
|
+
Tubeclip::Response::VideoSearch.new(
|
633
|
+
:feed_id => feed_id || nil,
|
634
|
+
:updated_at => updated_at || nil,
|
635
|
+
:total_result_count => total_result_count || nil,
|
636
|
+
:offset => offset || nil,
|
637
|
+
:max_result_count => max_result_count || nil,
|
638
|
+
:videos => videos)
|
639
|
+
end
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|