tmm1-youtube-g 0.4.5 → 0.4.9

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/History.txt CHANGED
@@ -1,5 +1,14 @@
1
1
  == trunk
2
-
2
+ * Added :page and :per_page options, this allows easier usage of the will_paginate
3
+ plugin with the library. The :offset and :max_results options are no longer available. [Daniel Insley]
4
+ * Added ability to get video responses on the instances of the YouTube::Model::Video object. [Daniel Insley]
5
+ * Added and improved the existing documentation [Daniel Insley]
6
+ * Fixed usage of deprecated yt:racy, now using media:rating [Daniel Insley]
7
+ * Renamed can_embed? method to embeddable? [Daniel Insley]
8
+ * Added ability for padingation and ordering on standard feeds. [Daniel Insley]
9
+ * Add error-handling for video upload errors. [FiXato]
10
+ * Add error-handling for authentication errors from YouTube during video upload. [FiXato]
11
+ * Add support for making videos private upon video upload. [FiXato]
3
12
  * Fix issue with REXML parsing of video upload response. [FiXato]
4
13
  * Fix issue with response code comparison. [FiXato]
5
14
  * Authcode is now retrieved for video uploads. [FiXato]
data/Manifest.txt CHANGED
@@ -17,6 +17,9 @@ lib/youtube_g/model/user.rb
17
17
  lib/youtube_g/model/video.rb
18
18
  lib/youtube_g/parser.rb
19
19
  lib/youtube_g/record.rb
20
+ lib/youtube_g/request/base_search.rb
21
+ lib/youtube_g/request/standard_search.rb
22
+ lib/youtube_g/request/user_search.rb
20
23
  lib/youtube_g/request/video_search.rb
21
24
  lib/youtube_g/request/video_upload.rb
22
25
  lib/youtube_g/response/video_search.rb
data/README.txt CHANGED
@@ -32,6 +32,7 @@ Create a client:
32
32
  Basic queries:
33
33
 
34
34
  client.videos_by(:query => "penguin")
35
+ client.videos_by(:query => "penguin", :page => 2, :per_page => 15)
35
36
  client.videos_by(:tags => ['tiger', 'leopard'])
36
37
  client.videos_by(:categories => [:news, :sports])
37
38
  client.videos_by(:categories => [:news, :sports], :tags => ['soccer', 'football'])
@@ -40,6 +41,7 @@ Basic queries:
40
41
  Standard feeds:
41
42
 
42
43
  client.videos_by(:most_viewed)
44
+ client.videos_by(:most_linked, :page => 3)
43
45
  client.videos_by(:top_rated, :time => :today)
44
46
 
45
47
  Advanced queries (with boolean operators OR (either), AND (include), NOT (exclude)):
data/Rakefile CHANGED
@@ -17,4 +17,9 @@ desc 'Tag release'
17
17
  task :tag do
18
18
  svn_root = 'svn+ssh://drummr77@rubyforge.org/var/svn/youtube-g'
19
19
  sh %(svn cp #{svn_root}/trunk #{svn_root}/tags/release-#{YouTubeG::VERSION} -m "Tag YouTubeG release #{YouTubeG::VERSION}")
20
+ end
21
+
22
+ desc 'Load the library in an IRB session'
23
+ task :console do
24
+ sh %(irb -r lib/youtube_g.rb)
20
25
  end
data/TODO.txt CHANGED
@@ -1,12 +1,10 @@
1
1
  [ ] stub out http request/response cycle for tests
2
- [ ] consider defaulting the client to no-logger, rather than outputting to STDOUT.
3
2
  [ ] allow specifying values as single items where you don't need to wrap in a list, e.g. :tags => :chickens instead of :tags => [ 'chickens' ]
4
3
  [ ] make sure symbols will work as well as tags everywhere (again, :tags => :chickens is same as :tags => 'chickens')
5
4
  [ ] figure out better structure for class/file (either rename request/video_search.rb or split into one class per file again)
6
5
  [ ] restore spaces after method def names
7
6
  [ ] use a proxy for testing with static sample result xml so we have repeatable tests
8
7
  [ ] Clean up tests using Shoulda to define contexts
9
- [ ] Consolidate requires
10
8
  [ ] Allow :category and :categories for query DSL
11
9
  [ ] Exception handling
12
10
 
data/lib/youtube_g.rb CHANGED
@@ -1,3 +1,10 @@
1
+ require 'logger'
2
+ require 'open-uri'
3
+ require 'net/https'
4
+ require 'digest/md5'
5
+ require 'rexml/document'
6
+ require 'cgi'
7
+
1
8
  require File.dirname(__FILE__) + '/youtube_g/client'
2
9
  require File.dirname(__FILE__) + '/youtube_g/record'
3
10
  require File.dirname(__FILE__) + '/youtube_g/parser'
@@ -10,10 +17,13 @@ require File.dirname(__FILE__) + '/youtube_g/model/rating'
10
17
  require File.dirname(__FILE__) + '/youtube_g/model/thumbnail'
11
18
  require File.dirname(__FILE__) + '/youtube_g/model/user'
12
19
  require File.dirname(__FILE__) + '/youtube_g/model/video'
20
+ require File.dirname(__FILE__) + '/youtube_g/request/base_search'
21
+ require File.dirname(__FILE__) + '/youtube_g/request/user_search'
22
+ require File.dirname(__FILE__) + '/youtube_g/request/standard_search'
13
23
  require File.dirname(__FILE__) + '/youtube_g/request/video_upload'
14
24
  require File.dirname(__FILE__) + '/youtube_g/request/video_search'
15
25
  require File.dirname(__FILE__) + '/youtube_g/response/video_search'
16
26
 
17
- class YouTubeG
27
+ class YouTubeG #:nodoc:
18
28
  VERSION = '0.4.5'
19
29
  end
@@ -1,36 +1,81 @@
1
- require 'logger'
2
-
3
1
  class YouTubeG
4
2
  class Client
5
3
  attr_accessor :logger
6
4
 
7
- def initialize(logger=Logger.new(STDOUT))
8
- @logger = logger
5
+ def initialize(logger=false)
6
+ @logger = Logger.new(STDOUT) if logger
9
7
  end
10
-
11
- # Params can be one of :most_viewed, :top_rated, :recently_featured, :watch_on_mobile
12
- # Or :tags, :categories, :query, :user
8
+
9
+ # Retrieves an array of standard feed, custom query, or user videos.
10
+ #
11
+ # === Parameters
12
+ # If fetching videos for a standard feed:
13
+ # params<Symbol>:: Accepts a symbol of :top_rated, :top_favorites, :most_viewed,
14
+ # :most_popular, :most_recent, :most_discussed, :most_linked,
15
+ # :most_responded, :recently_featured, and :watch_on_mobile.
16
+ #
17
+ # You can find out more specific information about what each standard feed provides
18
+ # by visiting: http://code.google.com/apis/youtube/reference.html#Standard_feeds
19
+ #
20
+ # options<Hash> (optional):: Accepts the options of :time, :page (default is 1),
21
+ # and :per_page (default is 25). :offset and :max_results
22
+ # can also be passed for a custom offset.
23
+ #
24
+ # If fetching videos by tags, categories, query:
25
+ # params<Hash>:: Accepts the keys :tags, :categories, :query, :order_by,
26
+ # :author, :racy, :response_format, :video_format, :page (default is 1),
27
+ # and :per_page(default is 25)
28
+ #
29
+ # options<Hash>:: Not used. (Optional)
30
+ #
31
+ # If fetching videos for a particular user:
32
+ # params<Hash>:: Key of :user with a value of the username.
33
+ # options<Hash>:: Not used. (Optional)
34
+ # === Returns
35
+ # YouTubeG::Response::VideoSearch
13
36
  def videos_by(params, options={})
37
+ request_params = params.respond_to?(:to_hash) ? params : options
38
+ request_params[:page] ||= 1
39
+
40
+ unless request_params[:max_results]
41
+ request_params[:max_results] = request_params[:per_page] || 25
42
+ end
43
+
44
+ unless request_params[:offset]
45
+ request_params[:offset] = calculate_offset(request_params[:page], request_params[:max_results] )
46
+ end
47
+
14
48
  if params.respond_to?(:to_hash) and not params[:user]
15
- request = YouTubeG::Request::VideoSearch.new(params)
16
-
49
+ request = YouTubeG::Request::VideoSearch.new(request_params)
17
50
  elsif (params.respond_to?(:to_hash) && params[:user]) || (params == :favorites)
18
- request = YouTubeG::Request::UserSearch.new(params, options)
19
-
51
+ request = YouTubeG::Request::UserSearch.new(request_params, options)
20
52
  else
21
- request = YouTubeG::Request::StandardSearch.new(params, options)
53
+ request = YouTubeG::Request::StandardSearch.new(params, request_params)
22
54
  end
23
55
 
24
- logger.debug "Submitting request [url=#{request.url}]."
56
+ logger.debug "Submitting request [url=#{request.url}]." if logger
25
57
  parser = YouTubeG::Parser::VideosFeedParser.new(request.url)
26
58
  parser.parse
27
59
  end
28
60
 
61
+ # Retrieves a single YouTube video.
62
+ #
63
+ # === Parameters
64
+ # vid<String>:: The ID or URL of the video that you'd like to retrieve.
65
+ #
66
+ # === Returns
67
+ # YouTubeG::Model::Video
29
68
  def video_by(vid)
30
69
  video_id = vid =~ /^http/ ? vid : "http://gdata.youtube.com/feeds/videos/#{vid}"
31
70
  parser = YouTubeG::Parser::VideoFeedParser.new(video_id)
32
71
  parser.parse
33
72
  end
34
73
 
74
+ private
75
+
76
+ def calculate_offset(page, per_page)
77
+ page == 1 ? 1 : ((per_page * page) - per_page + 1)
78
+ end
79
+
35
80
  end
36
81
  end
@@ -1,5 +1,3 @@
1
- require 'logger'
2
-
3
1
  class YouTubeG
4
2
 
5
3
  # TODO: Why is this needed? Does this happen if running standalone w/o Rails?
@@ -1,7 +1,10 @@
1
1
  class YouTubeG
2
2
  module Model
3
3
  class Author < YouTubeG::Record
4
+ # *String*: Author's YouTube username.
4
5
  attr_reader :name
6
+
7
+ # *String*: Feed URL of the author.
5
8
  attr_reader :uri
6
9
  end
7
10
  end
@@ -1,7 +1,10 @@
1
1
  class YouTubeG
2
2
  module Model
3
3
  class Category < YouTubeG::Record
4
- attr_reader :label
4
+ # *String*:: Name of the YouTube category
5
+ attr_reader :label
6
+
7
+ # *String*:: Identifies the type of item described.
5
8
  attr_reader :term
6
9
  end
7
10
  end
@@ -1,7 +1,15 @@
1
1
  class YouTubeG
2
2
  module Model
3
3
  class Contact < YouTubeG::Record
4
+ # *String*:: Identifies the status of a contact.
5
+ #
6
+ # * The tag's value will be accepted if the authenticated user and the contact have marked each other as friends.
7
+ # * The tag's value will be requested if the contact has asked to be added to the authenticated user's contact list, but the request has not yet been accepted (or rejected).
8
+ # * The tag's value will be pending if the authenticated user has asked to be added to the contact's contact list, but the request has not yet been accepted or rejected.
9
+ #
4
10
  attr_reader :status
11
+
12
+ # *String*:: The Youtube username of the contact.
5
13
  attr_reader :username
6
14
  end
7
15
  end
@@ -1,10 +1,15 @@
1
1
  class YouTubeG
2
2
  module Model
3
3
  class Content < YouTubeG::Record
4
+ # *Boolean*:: Description of the video.
4
5
  attr_reader :default
6
+ # *Fixnum*:: Length of the video in seconds.
5
7
  attr_reader :duration
8
+ # YouTubeG::Model::Video::Format:: Specifies the video format of the video object
6
9
  attr_reader :format
10
+ # *String*:: Specifies the MIME type of the media object.
7
11
  attr_reader :mime_type
12
+ # *String*:: Specifies the URL for the media object.
8
13
  attr_reader :url
9
14
 
10
15
  alias :is_default? :default
@@ -1,6 +1,7 @@
1
1
  class YouTubeG
2
2
  module Model
3
3
  class Playlist < YouTubeG::Record
4
+ # *String*:: User entered description for the playlist.
4
5
  attr_reader :description
5
6
  end
6
7
  end
@@ -1,9 +1,16 @@
1
1
  class YouTubeG
2
2
  module Model
3
3
  class Rating < YouTubeG::Record
4
+ # *Float*:: Average rating given to the video
4
5
  attr_reader :average
6
+
7
+ # *Fixnum*:: Maximum rating that can be assigned to the video
5
8
  attr_reader :max
9
+
10
+ # *Fixnum*:: Minimum rating that can be assigned to the video
6
11
  attr_reader :min
12
+
13
+ # *Fixnum*:: Indicates how many people have rated the video
7
14
  attr_reader :rater_count
8
15
  end
9
16
  end
@@ -1,9 +1,16 @@
1
1
  class YouTubeG
2
2
  module Model
3
3
  class Thumbnail < YouTubeG::Record
4
+ # *String*:: URL for the thumbnail image.
4
5
  attr_reader :url
6
+
7
+ # *Fixnum*:: Height of the thumbnail image.
5
8
  attr_reader :height
9
+
10
+ # *Fixnum*:: Width of the thumbnail image.
6
11
  attr_reader :width
12
+
13
+ # *String*:: Specifies the time offset at which the frame shown in the thumbnail image appears in the video.
7
14
  attr_reader :time
8
15
  end
9
16
  end
@@ -1,91 +1,165 @@
1
+ # TODO
2
+ # * self atom feed
3
+ # * alternate youtube watch url
4
+ # * comments feedLink
5
+
1
6
  class YouTubeG
2
7
  module Model
3
8
  class Video < YouTubeG::Record
4
9
  # Describes the various file formats in which a Youtube video may be
5
10
  # made available and allows looking them up by format code number.
6
- #
7
11
  class Format
8
12
  @@formats = Hash.new
9
13
 
14
+ # Instantiates a new video format object.
15
+ #
16
+ # == Parameters
17
+ # :format_code<Fixnum>:: The Youtube Format code of the object.
18
+ # :name<Symbol>:: The name of the format
19
+ #
20
+ # == Returns
21
+ # YouTubeG::Model::Video::Format: Video format object
10
22
  def initialize(format_code, name)
11
23
  @format_code = format_code
12
24
  @name = name
13
25
 
14
26
  @@formats[format_code] = self
15
27
  end
16
-
28
+
29
+ # Allows you to get the video format for a specific format code.
30
+ #
31
+ # A full list of format codes is available at:
32
+ #
33
+ # http://code.google.com/apis/youtube/reference.html#youtube_data_api_tag_media:content
34
+ #
35
+ # == Parameters
36
+ # :format_code<Fixnum>:: The Youtube Format code of the object.
37
+ #
38
+ # == Returns
39
+ # YouTubeG::Model::Video::Format: Video format object
17
40
  def self.by_code(format_code)
18
41
  @@formats[format_code]
19
42
  end
20
43
 
21
- # Flash format on YouTube site. All videos are available in this
22
- # format.
23
- #
44
+ # Flash format on YouTube site. All videos are available in this format.
24
45
  FLASH = YouTubeG::Model::Video::Format.new(0, :flash)
25
46
 
26
- # RTSP streaming URL for mobile video playback. H.263 video (176x144)
27
- # and AMR audio.
28
- #
47
+ # RTSP streaming URL for mobile video playback. H.263 video (176x144) and AMR audio.
29
48
  RTSP = YouTubeG::Model::Video::Format.new(1, :rtsp)
30
49
 
31
50
  # HTTP URL to the embeddable player (SWF) for this video. This format
32
51
  # is not available for a video that is not embeddable.
33
- #
34
52
  SWF = YouTubeG::Model::Video::Format.new(5, :swf)
35
53
 
54
+ # RTSP streaming URL for mobile video playback. MPEG-4 SP video (up to 176x144) and AAC audio.
36
55
  THREE_GPP = YouTubeG::Model::Video::Format.new(6, :three_gpp)
37
56
  end
38
-
57
+
58
+ # *Fixnum*:: Duration of a video in seconds.
39
59
  attr_reader :duration
60
+
61
+ # *Boolean*:: Specifies that a video may or may not be embedded on other websites.
40
62
  attr_reader :noembed
63
+
64
+ # *Fixnum*:: Specifies the order in which the video appears in a playlist.
41
65
  attr_reader :position
66
+
67
+ # *Boolean*:: Specifies that a video is flagged as adult or not.
42
68
  attr_reader :racy
43
- attr_reader :statistics
44
69
 
70
+ # *String*: Specifies a URI that uniquely and permanently identifies the video.
45
71
  attr_reader :video_id
72
+
73
+ # *Time*:: When the video was published on Youtube.
46
74
  attr_reader :published_at
75
+
76
+ # *Time*:: When the video's data was last updated.
47
77
  attr_reader :updated_at
78
+
79
+ # *Array*:: A array of YouTubeG::Model::Category objects that describe the videos categories.
48
80
  attr_reader :categories
81
+
82
+ # *Array*:: An array of words associated with the video.
49
83
  attr_reader :keywords
84
+
85
+ # *String*:: Description of the video.
50
86
  attr_reader :description
87
+
88
+ # *String*:: Title for the video.
51
89
  attr_reader :title
90
+
91
+ # *String*:: Description of the video.
52
92
  attr_reader :html_content
93
+
94
+ # YouTubeG::Model::Author:: Information about the YouTube user who owns a piece of video content.
53
95
  attr_reader :author
54
-
55
- # YouTubeG::Model::Content records describing the individual media content
56
- # data available for this video. Most, but not all, videos offer this.
96
+
97
+ # *Array*:: An array of YouTubeG::Model::Content objects describing the individual media content data available for this video. Most, but not all, videos offer this.
57
98
  attr_reader :media_content
58
-
59
- attr_reader :thumbnails # YouTubeG::Model::Thumbnail records
99
+
100
+ # *Array*:: An array of YouTubeG::Model::Thumbnail objects that contain information regarding the videos thumbnail images.
101
+ attr_reader :thumbnails
102
+
103
+ # *String*:: The link to watch the URL on YouTubes website.
60
104
  attr_reader :player_url
105
+
106
+ # YouTubeG::Model::Rating:: Information about the videos rating.
61
107
  attr_reader :rating
108
+
109
+ # *Fixnum*:: Number of times that the video has been viewed
62
110
  attr_reader :view_count
63
-
64
- # TODO:
65
- # self atom feed
66
- # alternate youtube watch url
67
- # responses feed
68
- # comments feedLink
69
-
111
+
112
+ attr_reader :statistics
113
+
114
+ # Videos related to the current video.
115
+ #
116
+ # === Returns
117
+ # YouTubeG::Response::VideoSearch
70
118
  def related
71
119
  YouTubeG::Parser::VideosFeedParser.new("http://gdata.youtube.com/feeds/api/videos/#{unique_id}/related").parse
72
120
  end
73
121
 
74
- # For convenience, the video_id with the URL stripped out, useful for searching for the video again
75
- # without having to store it anywhere. A regular query search, with this id will return the same video.
76
- # http://gdata.youtube.com/feeds/videos/ZTUVgYoeN_o
122
+ # Video responses to the current video.
123
+ #
124
+ # === Returns
125
+ # YouTubeG::Response::VideoSearch
126
+ def responses
127
+ YouTubeG::Parser::VideosFeedParser.new("http://gdata.youtube.com/feeds/api/videos/#{unique_id}/responses").parse
128
+ end
129
+
130
+ # The ID of the video, useful for searching for the video again without having to store it anywhere.
131
+ # A regular query search, with this id will return the same video.
132
+ #
133
+ # === Example
134
+ # >> video.unique_id
135
+ # => "ZTUVgYoeN_o"
136
+ #
137
+ # === Returns
138
+ # String: The Youtube video id.
77
139
  def unique_id
78
140
  video_id[/videos\/([^<]+)/, 1]
79
141
  end
80
142
 
81
- def can_embed?
143
+ # Allows you to check whether the video can be embedded on a webpage.
144
+ #
145
+ # === Returns
146
+ # Boolean: True if the video can be embedded, false if not.
147
+ def embeddable?
82
148
  not @noembed
83
149
  end
84
150
 
151
+ # Provides a URL and various other types of information about a video.
152
+ #
153
+ # === Returns
154
+ # YouTubeG::Model::Content: Data about the embeddable video.
85
155
  def default_media_content
86
156
  @media_content.find { |c| c.is_default? }
87
157
  end
88
158
 
159
+ # Gives you the HTML to embed the video on your website.
160
+ #
161
+ # === Returns
162
+ # String: The HTML for embedding the video on your website.
89
163
  def embed_html(width = 425, height = 350)
90
164
  <<EDOC
91
165
  <object width="#{width}" height="#{height}">
@@ -97,6 +171,10 @@ class YouTubeG
97
171
  EDOC
98
172
  end
99
173
 
174
+ # The URL needed for embedding the video in a page.
175
+ #
176
+ # === Returns
177
+ # String: Absolute URL for embedding video
100
178
  def embed_url
101
179
  @player_url.sub('watch?', '').sub('=', '/')
102
180
  end
@@ -1,10 +1,6 @@
1
- require 'cgi'
2
- require 'open-uri'
3
- require 'rexml/document'
4
-
5
1
  class YouTubeG
6
- module Parser
7
- class FeedParser
2
+ module Parser #:nodoc:
3
+ class FeedParser #:nodoc:
8
4
  def initialize(url)
9
5
  @url = url
10
6
  end
@@ -14,17 +10,16 @@ class YouTubeG
14
10
  end
15
11
  end
16
12
 
17
- class VideoFeedParser < FeedParser
13
+ class VideoFeedParser < FeedParser #:nodoc:
18
14
 
19
15
  def parse_content(content)
20
16
  doc = REXML::Document.new(content)
21
17
  entry = doc.elements["entry"]
22
-
23
18
  parse_entry(entry)
24
19
  end
25
20
 
26
21
  protected
27
- def parse_entry(entry)
22
+ def parse_entry(entry)
28
23
  video_id = entry.elements["id"].text
29
24
  published_at = Time.parse(entry.elements["published"].text)
30
25
  updated_at = Time.parse(entry.elements["updated"].text)
@@ -94,7 +89,7 @@ class YouTubeG
94
89
  view_count = (el = entry.elements["yt:statistics"]) ? el.attributes["viewCount"].to_i : 0
95
90
 
96
91
  noembed = entry.elements["yt:noembed"] ? true : false
97
- racy = entry.elements["yt:racy"] ? true : false
92
+ racy = entry.elements["media:rating"] ? true : false
98
93
 
99
94
  YouTubeG::Model::Video.new(
100
95
  :video_id => video_id,
@@ -116,7 +111,7 @@ class YouTubeG
116
111
  :racy => racy)
117
112
  end
118
113
 
119
- def parse_media_content (media_content_element)
114
+ def parse_media_content (media_content_element)
120
115
  content_url = media_content_element.attributes["url"]
121
116
  format_code = media_content_element.attributes["yt:format"].to_i
122
117
  format = YouTubeG::Model::Video::Format.by_code(format_code)
@@ -133,10 +128,10 @@ class YouTubeG
133
128
  end
134
129
  end
135
130
 
136
- class VideosFeedParser < VideoFeedParser
131
+ class VideosFeedParser < VideoFeedParser #:nodoc:
137
132
 
138
133
  private
139
- def parse_content(content)
134
+ def parse_content(content) #:nodoc:
140
135
  doc = REXML::Document.new(content)
141
136
  feed = doc.elements["feed"]
142
137
 
@@ -1,5 +1,5 @@
1
1
  class YouTubeG
2
- class Record
2
+ class Record #:nodoc:
3
3
  def initialize (params)
4
4
  return if params.nil?
5
5
 
@@ -1,48 +1,6 @@
1
1
  class YouTubeG
2
-
3
- # The goal of the classes in this module is to build the request URLs for each type of search
4
- module Request
5
-
6
- class BaseSearch
7
- attr_reader :url
8
-
9
- def base_url
10
- "http://gdata.youtube.com/feeds/api/"
11
- end
12
- end
13
-
14
- class UserSearch < BaseSearch
15
-
16
- def initialize(params, options={})
17
- @url = base_url
18
- return @url << "#{options[:user]}/favorites" if params == :favorites
19
- @url << "#{params[:user]}/uploads" if params[:user]
20
- end
21
-
22
- def base_url
23
- super << "users/"
24
- end
25
- end
26
-
27
- class StandardSearch < BaseSearch
28
- TYPES = [ :most_viewed, :top_rated, :recently_featured, :watch_on_mobile ]
29
- TIMES = [ :all_time, :today, :this_week, :this_month ]
30
-
31
- def initialize(type, options={})
32
- if TYPES.include?(type)
33
- @url = base_url << type.to_s
34
- @url << "?time=#{CGI.escape(options.delete(:time).to_s)}" if TIMES.include?(options[:time])
35
- else
36
- raise "Invalid type, must be one of: #{ TYPES.map { |t| t.to_s }.join(", ") }"
37
- end
38
- end
39
-
40
- def base_url
41
- super << "standardfeeds/"
42
- end
43
- end
44
-
45
- class VideoSearch < BaseSearch
2
+ module Request #:nodoc:
3
+ class VideoSearch < BaseSearch #:nodoc:
46
4
  # From here: http://code.google.com/apis/youtube/reference.html#yt_format
47
5
  ONLY_EMBEDDABLE = 5
48
6
 
@@ -58,34 +16,22 @@ class YouTubeG
58
16
  attr_reader :author
59
17
 
60
18
  def initialize(params={})
61
- # XXX I think we want to delete the line below
62
- return if params.nil?
63
-
64
- # initialize our various member data to avoid warnings and so we'll
19
+ # Initialize our various member data to avoid warnings and so we'll
65
20
  # automatically fall back to the youtube api defaults
66
- @max_results = nil
67
- @order_by = nil
68
- @offset = nil
69
- @query = nil
70
- @response_format = nil
71
- @video_format = nil
72
- @racy = nil
73
- @author = nil
74
-
75
- # build up the url corresponding to this request
21
+ @max_results, @order_by,
22
+ @offset, @query,
23
+ @response_format, @video_format,
24
+ @racy, @author = nil
76
25
  @url = base_url
77
26
 
78
- # http://gdata.youtube.com/feeds/videos/T7YazwP8GtY
27
+ # Return a single video (base_url + /T7YazwP8GtY)
79
28
  return @url << "/" << params[:video_id] if params[:video_id]
80
29
 
81
30
  @url << "/-/" if (params[:categories] || params[:tags])
82
31
  @url << categories_to_params(params.delete(:categories)) if params[:categories]
83
32
  @url << tags_to_params(params.delete(:tags)) if params[:tags]
84
33
 
85
- params.each do |key, value|
86
- name = key.to_s
87
- instance_variable_set("@#{name}", value) if respond_to?(name)
88
- end
34
+ set_instance_variables(params)
89
35
 
90
36
  if( params[ :only_embeddable ] )
91
37
  @video_format = ONLY_EMBEDDABLE
@@ -94,11 +40,13 @@ class YouTubeG
94
40
  @url << build_query_params(to_youtube_params)
95
41
  end
96
42
 
97
- def base_url
43
+ private
44
+
45
+ def base_url #:nodoc:
98
46
  super << "videos"
99
47
  end
100
48
 
101
- def to_youtube_params
49
+ def to_youtube_params #:nodoc:
102
50
  {
103
51
  'max-results' => @max_results,
104
52
  'orderby' => @order_by,
@@ -110,58 +58,35 @@ class YouTubeG
110
58
  'author' => @author
111
59
  }
112
60
  end
113
-
114
- private
115
- # Convert category symbols into strings and build the URL. GData requires categories to be capitalized.
116
- # Categories defined like: categories => { :include => [:news], :exclude => [:sports], :either => [..] }
117
- # or like: categories => [:news, :sports]
118
- def categories_to_params(categories)
119
- if categories.respond_to?(:keys) and categories.respond_to?(:[])
120
- s = ""
121
- s << categories[:either].map { |c| c.to_s.capitalize }.join("%7C") << '/' if categories[:either]
122
- s << categories[:include].map { |c| c.to_s.capitalize }.join("/") << '/' if categories[:include]
123
- s << ("-" << categories[:exclude].map { |c| c.to_s.capitalize }.join("/-")) << '/' if categories[:exclude]
124
- s
125
- else
126
- categories.map { |c| c.to_s.capitalize }.join("/") << '/'
127
- end
128
- end
129
-
130
- # Tags defined like: tags => { :include => [:football], :exclude => [:soccer], :either => [:polo, :tennis] }
131
- # or tags => [:football, :soccer]
132
- def tags_to_params(tags)
133
- if tags.respond_to?(:keys) and tags.respond_to?(:[])
134
- s = ""
135
- s << tags[:either].map { |t| CGI.escape(t.to_s) }.join("%7C") << '/' if tags[:either]
136
- s << tags[:include].map { |t| CGI.escape(t.to_s) }.join("/") << '/' if tags[:include]
137
- s << ("-" << tags[:exclude].map { |t| CGI.escape(t.to_s) }.join("/-")) << '/' if tags[:exclude]
138
- s
139
- else
140
- tags.map { |t| CGI.escape(t.to_s) }.join("/") << '/'
141
- end
142
- end
143
61
 
144
- def build_query_params(params)
145
- # nothing to do if there are no params
146
- return '' if (!params || params.empty?)
147
-
148
- # build up the query param string, tacking on every key/value
149
- # pair for which the value is non-nil
150
- u = '?'
151
- item_count = 0
152
- params.keys.each do |key|
153
- value = params[key]
154
- next if value.nil?
155
-
156
- u << '&' if (item_count > 0)
157
- u << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
158
- item_count += 1
159
- end
160
-
161
- # if we found no non-nil values, we've got no params so just
162
- # return an empty string
163
- (item_count == 0) ? '' : u
62
+ # Convert category symbols into strings and build the URL. GData requires categories to be capitalized.
63
+ # Categories defined like: categories => { :include => [:news], :exclude => [:sports], :either => [..] }
64
+ # or like: categories => [:news, :sports]
65
+ def categories_to_params(categories) #:nodoc:
66
+ if categories.respond_to?(:keys) and categories.respond_to?(:[])
67
+ s = ""
68
+ s << categories[:either].map { |c| c.to_s.capitalize }.join("%7C") << '/' if categories[:either]
69
+ s << categories[:include].map { |c| c.to_s.capitalize }.join("/") << '/' if categories[:include]
70
+ s << ("-" << categories[:exclude].map { |c| c.to_s.capitalize }.join("/-")) << '/' if categories[:exclude]
71
+ s
72
+ else
73
+ categories.map { |c| c.to_s.capitalize }.join("/") << '/'
164
74
  end
75
+ end
76
+
77
+ # Tags defined like: tags => { :include => [:football], :exclude => [:soccer], :either => [:polo, :tennis] }
78
+ # or tags => [:football, :soccer]
79
+ def tags_to_params(tags) #:nodoc:
80
+ if tags.respond_to?(:keys) and tags.respond_to?(:[])
81
+ s = ""
82
+ s << tags[:either].map { |t| CGI.escape(t.to_s) }.join("%7C") << '/' if tags[:either]
83
+ s << tags[:include].map { |t| CGI.escape(t.to_s) }.join("/") << '/' if tags[:include]
84
+ s << ("-" << tags[:exclude].map { |t| CGI.escape(t.to_s) }.join("/-")) << '/' if tags[:exclude]
85
+ s
86
+ else
87
+ tags.map { |t| CGI.escape(t.to_s) }.join("/") << '/'
88
+ end
89
+ end
165
90
 
166
91
  end
167
92
  end
@@ -1,12 +1,8 @@
1
- require 'net/https'
2
- require 'digest/md5'
3
- require 'rexml/document'
4
- require 'cgi'
5
-
6
1
  class YouTubeG
7
2
 
8
3
  module Upload
9
4
  class UploadError < Exception; end
5
+ class AuthenticationError < Exception; end
10
6
 
11
7
  # require 'youtube_g'
12
8
  #
@@ -32,8 +28,14 @@ class YouTubeG
32
28
  # :description
33
29
  # :category
34
30
  # :keywords
31
+ # :private
32
+ # Specifying :private will make the video private, otherwise it will be public.
35
33
  #
36
-
34
+ # When one of the fields is invalid according to YouTube,
35
+ # an UploadError will be returned. Its message contains a list of newline separated
36
+ # errors, containing the key and its error code.
37
+ #
38
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
37
39
  def upload data, opts = {}
38
40
  data = data.respond_to?(:read) ? data.read : data
39
41
  @opts = { :mime_type => 'video/mp4',
@@ -56,6 +58,19 @@ class YouTubeG
56
58
 
57
59
  Net::HTTP.start(base_url) do |upload|
58
60
  response = upload.post('/feeds/api/users/' << @user << '/uploads', uploadBody, uploadHeader)
61
+ if response.code.to_i == 403
62
+ raise AuthenticationError, response.body[/<TITLE>(.+)<\/TITLE>/, 1]
63
+ elsif response.code.to_i != 201
64
+ upload_error = ''
65
+ xml = REXML::Document.new(response.body)
66
+ errors = xml.elements["//errors"]
67
+ errors.each do |error|
68
+ location = error.elements["location"].text[/media:group\/media:(.*)\/text\(\)/,1]
69
+ code = error.elements["code"].text
70
+ upload_error << sprintf("%s: %s\r\n", location, code)
71
+ end
72
+ raise UploadError, upload_error
73
+ end
59
74
  xml = REXML::Document.new(response.body)
60
75
  return xml.elements["//id"].text[/videos\/(.+)/, 1]
61
76
  end
@@ -64,15 +79,15 @@ class YouTubeG
64
79
 
65
80
  private
66
81
 
67
- def base_url
82
+ def base_url #:nodoc:
68
83
  "uploads.gdata.youtube.com"
69
84
  end
70
85
 
71
- def boundary
86
+ def boundary #:nodoc:
72
87
  "An43094fu"
73
88
  end
74
89
 
75
- def auth_token
90
+ def auth_token #:nodoc:
76
91
  unless @auth_token
77
92
  http = Net::HTTP.new("www.google.com", 443)
78
93
  http.use_ssl = true
@@ -85,18 +100,21 @@ class YouTubeG
85
100
  @auth_token
86
101
  end
87
102
 
88
- def video_xml
89
- %[<?xml version="1.0"?>
90
- <entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">
91
- <media:group>
92
- <media:title type="plain">#{@opts[:title]}</media:title>
93
- <media:description type="plain">#{@opts[:description]}</media:description>
94
- <media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">#{@opts[:category]}</media:category>
95
- <media:keywords>#{@opts[:keywords].join ","}</media:keywords>
96
- </media:group></entry> ]
103
+ def video_xml #:nodoc:
104
+ video_xml = ''
105
+ video_xml << '<?xml version="1.0"?>'
106
+ video_xml << '<entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">'
107
+ video_xml << '<media:group>'
108
+ video_xml << '<media:title type="plain">%s</media:title>' % @opts[:title]
109
+ video_xml << '<media:description type="plain">%s</media:description>' % @opts[:description]
110
+ video_xml << '<media:keywords>%s</media:keywords>' % @opts[:keywords].join(",")
111
+ video_xml << '<media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">%s</media:category>' % @opts[:category]
112
+ video_xml << '<yt:private/>' if @opts[:private]
113
+ video_xml << '</media:group>'
114
+ video_xml << '</entry>'
97
115
  end
98
116
 
99
- def generate_upload_body(boundary, video_xml, data)
117
+ def generate_upload_body(boundary, video_xml, data) #:nodoc:
100
118
  uploadBody = ""
101
119
  uploadBody << "--#{boundary}\r\n"
102
120
  uploadBody << "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n"
@@ -1,22 +1,22 @@
1
1
  class YouTubeG
2
2
  module Response
3
3
  class VideoSearch < YouTubeG::Record
4
- # the unique feed identifying url
5
- attr_reader :feed_id
4
+ # *String*:: Unique feed identifying url.
5
+ attr_reader :feed_id
6
6
 
7
- # the number of results per page
7
+ # *Fixnum*:: Number of results per page.
8
8
  attr_reader :max_result_count
9
9
 
10
- # the 1-based offset index into the full result set
10
+ # *Fixnum*:: 1-based offset index into the full result set.
11
11
  attr_reader :offset
12
12
 
13
- # the total number of results available for the original request
13
+ # *Fixnum*:: Total number of results available for the original request.
14
14
  attr_reader :total_result_count
15
15
 
16
- # the date and time at which the feed was last updated
16
+ # *Time*:: Date and time at which the feed was last updated
17
17
  attr_reader :updated_at
18
18
 
19
- # the list of Video records
19
+ # *Array*:: Array of YouTubeG::Model::Video records
20
20
  attr_reader :videos
21
21
  end
22
22
  end
data/test/test_client.rb CHANGED
@@ -12,7 +12,7 @@ class TestClient < Test::Unit::TestCase
12
12
  def test_should_respond_to_a_basic_query
13
13
  response = @client.videos_by(:query => "penguin")
14
14
 
15
- assert_equal "http://gdata.youtube.com/feeds/api/videos?start-index=1&max-results=25&vq=penguin", response.feed_id
15
+ assert_equal "http://gdata.youtube.com/feeds/api/videos", response.feed_id
16
16
  assert_equal 25, response.max_result_count
17
17
  assert_equal 25, response.videos.length
18
18
  assert_equal 1, response.offset
@@ -22,10 +22,41 @@ class TestClient < Test::Unit::TestCase
22
22
  response.videos.each { |v| assert_valid_video v }
23
23
  end
24
24
 
25
+ def test_should_respond_to_a_basic_query_with_offset_and_max_results
26
+ response = @client.videos_by(:query => "penguin", :offset => 15, :max_results => 30)
27
+
28
+ assert_equal "http://gdata.youtube.com/feeds/api/videos", response.feed_id
29
+ assert_equal 30, response.max_result_count
30
+ assert_equal 30, response.videos.length
31
+ assert_equal 15, response.offset
32
+ assert(response.total_result_count > 100)
33
+ assert_instance_of Time, response.updated_at
34
+
35
+ response.videos.each { |v| assert_valid_video v }
36
+ end
37
+
38
+ def test_should_respond_to_a_basic_query_with_paging
39
+ response = @client.videos_by(:query => "penguin")
40
+ assert_equal "http://gdata.youtube.com/feeds/api/videos", response.feed_id
41
+ assert_equal 25, response.max_result_count
42
+ assert_equal 1, response.offset
43
+
44
+ response = @client.videos_by(:query => "penguin", :page => 2)
45
+ assert_equal "http://gdata.youtube.com/feeds/api/videos", response.feed_id
46
+ assert_equal 25, response.max_result_count
47
+ assert_equal 26, response.offset
48
+
49
+ response2 = @client.videos_by(:query => "penguin", :page => 3)
50
+ assert_equal "http://gdata.youtube.com/feeds/api/videos", response2.feed_id
51
+ assert_equal 25, response2.max_result_count
52
+ assert_equal 51, response2.offset
53
+ end
54
+
55
+
25
56
  def test_should_get_videos_for_multiword_metasearch_query
26
57
  response = @client.videos_by(:query => 'christina ricci')
27
58
 
28
- assert_equal "http://gdata.youtube.com/feeds/api/videos?start-index=1&max-results=25&vq=christina+ricci", response.feed_id
59
+ assert_equal "http://gdata.youtube.com/feeds/api/videos", response.feed_id
29
60
  assert_equal 25, response.max_result_count
30
61
  assert_equal 25, response.videos.length
31
62
  assert_equal 1, response.offset
@@ -109,19 +140,18 @@ class TestClient < Test::Unit::TestCase
109
140
  # end
110
141
 
111
142
  def test_should_get_videos_for_query_search_with_categories_excluded
112
- response = @client.videos_by(:query => 'bench press', :categories => { :exclude => [:comedy, :entertainment] },
113
- :max_results => 10)
114
- assert_equal "<object width=\"425\" height=\"350\">\n <param name=\"movie\" value=\"http://www.youtube.com/v/BlDWdfTAx8o\"></param>\n <param name=\"wmode\" value=\"transparent\"></param>\n <embed src=\"http://www.youtube.com/v/BlDWdfTAx8o\" type=\"application/x-shockwave-flash\" \n wmode=\"transparent\" width=\"425\" height=\"350\"></embed>\n</object>\n", response.videos.first.embed_html
115
- response.videos.each { |v| assert_valid_video v }
116
- end
117
-
118
- def test_should_be_able_to_pass_in_logger
119
- @client = YouTubeG::Client.new(Logger.new(STDOUT))
120
- assert_not_nil @client.logger
143
+ video = @client.video_by("EkF4JD2rO3Q")
144
+ assert_equal "<object width=\"425\" height=\"350\">\n <param name=\"movie\" value=\"http://www.youtube.com/v/EkF4JD2rO3Q\"></param>\n <param name=\"wmode\" value=\"transparent\"></param>\n <embed src=\"http://www.youtube.com/v/EkF4JD2rO3Q\" type=\"application/x-shockwave-flash\" \n wmode=\"transparent\" width=\"425\" height=\"350\"></embed>\n</object>\n", video.embed_html
145
+ assert_valid_video video
121
146
  end
122
147
 
123
- def test_should_create_logger_if_not_passed_in
148
+ def test_should_disable_debug_if_debug_is_set_to_false
124
149
  @client = YouTubeG::Client.new
150
+ assert_nil @client.logger
151
+ end
152
+
153
+ def test_should_enable_logger_if_debug_is_true
154
+ @client = YouTubeG::Client.new(true)
125
155
  assert_not_nil @client.logger
126
156
  end
127
157
 
@@ -129,14 +159,14 @@ class TestClient < Test::Unit::TestCase
129
159
  response = @client.videos_by(:query => "avril lavigne girlfriend")
130
160
 
131
161
  video = response.videos.first
132
- assert !video.can_embed?
162
+ assert !video.embeddable?
133
163
  end
134
164
 
135
165
  def test_should_determine_if_embeddable_video_is_embeddable
136
166
  response = @client.videos_by(:query => "strongbad")
137
167
 
138
168
  video = response.videos.first
139
- assert video.can_embed?
169
+ assert video.embeddable?
140
170
  end
141
171
 
142
172
  def test_should_retrieve_video_by_id
data/test/test_video.rb CHANGED
@@ -23,8 +23,20 @@ class TestVideo < Test::Unit::TestCase
23
23
  assert_equal 25, response.max_result_count
24
24
  assert_equal 25, response.videos.length
25
25
  assert_equal 1, response.offset
26
- puts response.total_result_count
27
26
  assert(response.total_result_count > 0)
28
27
  assert_instance_of Time, response.updated_at
29
28
  end
29
+
30
+ def test_should_have_response_videos
31
+ video = YouTubeG::Model::Video.new(:video_id => "http://gdata.youtube.com/feeds/videos/BDqs-OZWw9o")
32
+ response = video.responses
33
+
34
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/BDqs-OZWw9o/responses", response.feed_id
35
+ assert_equal 25, response.max_result_count
36
+ assert_equal 25, response.videos.length
37
+ assert_equal 1, response.offset
38
+ assert(response.total_result_count > 0)
39
+ assert_instance_of Time, response.updated_at
40
+ end
41
+
30
42
  end
@@ -61,17 +61,27 @@ class TestVideoSearch < Test::Unit::TestCase
61
61
  request = YouTubeG::Request::StandardSearch.new(:most_viewed)
62
62
  assert_equal "http://gdata.youtube.com/feeds/api/standardfeeds/most_viewed", request.url
63
63
  end
64
+
65
+ def test_should_build_url_for_top_rated_for_today
66
+ request = YouTubeG::Request::StandardSearch.new(:top_rated, :time => :today)
67
+ assert_equal "http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?time=today", request.url
68
+ end
69
+
70
+ def test_should_build_url_for_most_viewed_offset_and_max_results_without_time
71
+ request = YouTubeG::Request::StandardSearch.new(:top_rated, :offset => 5, :max_results => 10)
72
+ assert_equal "http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?max-results=10&start-index=5", request.url
73
+ end
74
+
75
+ def test_should_build_url_for_most_viewed_offset_and_max_results_with_time
76
+ request = YouTubeG::Request::StandardSearch.new(:top_rated, :offset => 5, :max_results => 10, :time => :today)
77
+ assert_equal "http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?time=today&max-results=10&start-index=5", request.url
78
+ end
64
79
 
65
80
  def test_should_raise_exception_for_invalid_type
66
81
  assert_raise RuntimeError do
67
82
  request = YouTubeG::Request::StandardSearch.new(:most_viewed_yo)
68
83
  end
69
84
  end
70
-
71
- def test_should_build_url_for_top_rated_for_today
72
- request = YouTubeG::Request::StandardSearch.new(:top_rated, :time => :today)
73
- assert_equal "http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?time=today", request.url
74
- end
75
85
 
76
86
  # -- Complex Video Queries -------------------------------------------------------------------------
77
87
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tmm1-youtube-g
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane Vitarana