msp-youtube-g 0.4.5

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 ADDED
@@ -0,0 +1,30 @@
1
+ == trunk
2
+
3
+ * Fix issue with REXML parsing of video upload response. [FiXato]
4
+ * Fix issue with response code comparison. [FiXato]
5
+ * Authcode is now retrieved for video uploads. [FiXato]
6
+ * Add basic support for uploading videos [thanks Joe Damato]
7
+ * Add basic support for related videos [tmm1]
8
+ * Improve docs for order_by attribute [thanks Jason Arora]
9
+ * Added support for the "racy" parameter (choices are "include" or "exclude") [thanks Jason Arora]
10
+ * Add missing attribute reader for description [tmm1]
11
+ * Fix issue with missing yt:statistics and viewCount [tmm1]
12
+ * Allow Client#video_by to take either a url or a video id [tmm1]
13
+
14
+ == 0.4.1 / 2008-02-11
15
+
16
+ * Added 3GPP video format [shane]
17
+ * Fixed tests [shane]
18
+
19
+ == 0.4.0 / 2007-12-18
20
+
21
+ * Fixed API projection in search URL [Pete Higgins]
22
+ * Fixed embeddable video searching [Pete Higgins]
23
+ * Fixed video embeddable detection [Pete Higgins]
24
+ * Fixed unique id hyphen detection [Pete Higgins, Chris Taggart]
25
+
26
+ == 0.3.0 / 2007-09-17
27
+
28
+ * Initial public release
29
+ * Birthday!
30
+
data/Manifest.txt ADDED
@@ -0,0 +1,25 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ TODO.txt
6
+ lib/youtube_g.rb
7
+ lib/youtube_g/client.rb
8
+ lib/youtube_g/logger.rb
9
+ lib/youtube_g/model/author.rb
10
+ lib/youtube_g/model/category.rb
11
+ lib/youtube_g/model/contact.rb
12
+ lib/youtube_g/model/content.rb
13
+ lib/youtube_g/model/playlist.rb
14
+ lib/youtube_g/model/rating.rb
15
+ lib/youtube_g/model/thumbnail.rb
16
+ lib/youtube_g/model/user.rb
17
+ lib/youtube_g/model/video.rb
18
+ lib/youtube_g/parser.rb
19
+ lib/youtube_g/record.rb
20
+ lib/youtube_g/request/video_search.rb
21
+ lib/youtube_g/request/video_upload.rb
22
+ lib/youtube_g/response/video_search.rb
23
+ test/test_client.rb
24
+ test/test_video.rb
25
+ test/test_video_search.rb
data/README.txt ADDED
@@ -0,0 +1,81 @@
1
+ youtube-g
2
+ by Shane Vitarana and Walter Korman
3
+
4
+ Rubyforge: http://rubyforge.org/projects/youtube-g/
5
+ RDoc: http://youtube-g.rubyforge.org/
6
+ Google Group: http://groups.google.com/group/ruby-youtube-library
7
+
8
+ == DESCRIPTION:
9
+
10
+ youtube-g is a pure Ruby client for the YouTube GData API. It provides an easy
11
+ way to access the latest YouTube video search results from your own programs.
12
+ In comparison with the earlier Youtube search interfaces, this new API and
13
+ library offers much-improved flexibility around executing complex search
14
+ queries to obtain well-targeted video search results.
15
+
16
+ More detail on the underlying source Google-provided API is available at:
17
+
18
+ http://code.google.com/apis/youtube/overview.html
19
+
20
+ == FEATURES/PROBLEMS:
21
+
22
+ * Aims to be in parity with Google's YouTube GData API. Core functionality
23
+ is currently present -- work is in progress to fill in the rest.
24
+
25
+ == SYNOPSIS:
26
+
27
+ Create a client:
28
+
29
+ require 'youtube_g'
30
+ client = YouTubeG::Client.new
31
+
32
+ Basic queries:
33
+
34
+ client.videos_by(:query => "penguin")
35
+ client.videos_by(:tags => ['tiger', 'leopard'])
36
+ client.videos_by(:categories => [:news, :sports])
37
+ client.videos_by(:categories => [:news, :sports], :tags => ['soccer', 'football'])
38
+ client.videos_by(:user => 'liz')
39
+
40
+ Standard feeds:
41
+
42
+ client.videos_by(:most_viewed)
43
+ client.videos_by(:top_rated, :time => :today)
44
+
45
+ Advanced queries (with boolean operators OR (either), AND (include), NOT (exclude)):
46
+
47
+ client.videos_by(:categories => { :either => [:news, :sports], :exclude => [:comedy] }, :tags => { :include => ['football'], :exclude => ['soccer'] })
48
+
49
+
50
+ == REQUIREMENTS:
51
+
52
+ * None
53
+
54
+ == INSTALL:
55
+
56
+ * sudo gem install youtube-g
57
+
58
+ == LICENSE:
59
+
60
+ MIT License
61
+
62
+ Copyright (c) 2007 Shane Vitarana and Walter Korman
63
+
64
+ Permission is hereby granted, free of charge, to any person obtaining
65
+ a copy of this software and associated documentation files (the
66
+ 'Software'), to deal in the Software without restriction, including
67
+ without limitation the rights to use, copy, modify, merge, publish,
68
+ distribute, sublicense, and/or sell copies of the Software, and to
69
+ permit persons to whom the Software is furnished to do so, subject to
70
+ the following conditions:
71
+
72
+ The above copyright notice and this permission notice shall be
73
+ included in all copies or substantial portions of the Software.
74
+
75
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
76
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
77
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
78
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
79
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
80
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
81
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require 'lib/youtube_g'
4
+
5
+ Hoe.new('youtube-g', YouTubeG::VERSION) do |p|
6
+ p.rubyforge_name = 'youtube-g'
7
+ p.author = ["Shane Vitarana", "Walter Korman", "Aman Gupta", "Filip H.F. Slagter"]
8
+ p.email = 'shanev@gmail.com'
9
+ p.summary = 'Ruby client for the YouTube GData API'
10
+ p.description = p.paragraphs_of('README.txt', 2..8).join("\n\n")
11
+ p.url = 'http://rubyforge.org/projects/youtube-g/'
12
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
13
+ p.remote_rdoc_dir = ''
14
+ end
15
+
16
+ desc 'Tag release'
17
+ task :tag do
18
+ svn_root = 'svn+ssh://drummr77@rubyforge.org/var/svn/youtube-g'
19
+ sh %(svn cp #{svn_root}/trunk #{svn_root}/tags/release-#{YouTubeG::VERSION} -m "Tag YouTubeG release #{YouTubeG::VERSION}")
20
+ end
data/TODO.txt ADDED
@@ -0,0 +1,18 @@
1
+ [ ] stub out http request/response cycle for tests
2
+ [ ] consider defaulting the client to no-logger, rather than outputting to STDOUT.
3
+ [ ] 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
+ [ ] make sure symbols will work as well as tags everywhere (again, :tags => :chickens is same as :tags => 'chickens')
5
+ [ ] figure out better structure for class/file (either rename request/video_search.rb or split into one class per file again)
6
+ [ ] restore spaces after method def names
7
+ [ ] use a proxy for testing with static sample result xml so we have repeatable tests
8
+ [ ] Clean up tests using Shoulda to define contexts
9
+ [ ] Consolidate requires
10
+ [ ] Allow :category and :categories for query DSL
11
+ [ ] Exception handling
12
+
13
+ == API Features TODO
14
+
15
+ [ ] Profile feed parsing
16
+ [ ] Playlist feeds
17
+ [ ] User subscriptions
18
+ [ ] Video comments
@@ -0,0 +1,36 @@
1
+ require 'logger'
2
+
3
+ class YouTubeG
4
+ class Client
5
+ attr_accessor :logger
6
+
7
+ def initialize(logger=Logger.new(STDOUT))
8
+ @logger = logger
9
+ end
10
+
11
+ # Params can be one of :most_viewed, :top_rated, :recently_featured, :watch_on_mobile
12
+ # Or :tags, :categories, :query, :user
13
+ def videos_by(params, options={})
14
+ if params.respond_to?(:to_hash) and not params[:user]
15
+ request = YouTubeG::Request::VideoSearch.new(params)
16
+
17
+ elsif (params.respond_to?(:to_hash) && params[:user]) || (params == :favorites)
18
+ request = YouTubeG::Request::UserSearch.new(params, options)
19
+
20
+ else
21
+ request = YouTubeG::Request::StandardSearch.new(params, options)
22
+ end
23
+
24
+ logger.debug "Submitting request [url=#{request.url}]."
25
+ parser = YouTubeG::Parser::VideosFeedParser.new(request.url)
26
+ parser.parse
27
+ end
28
+
29
+ def video_by(vid)
30
+ video_id = vid =~ /^http/ ? vid : "http://gdata.youtube.com/feeds/videos/#{vid}"
31
+ parser = YouTubeG::Parser::VideoFeedParser.new(video_id)
32
+ parser.parse
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ require 'logger'
2
+
3
+ class YouTubeG
4
+
5
+ # TODO: Why is this needed? Does this happen if running standalone w/o Rails?
6
+ # Anyway, isn't it easier to debug w/o the really long timestamp & log level?
7
+ # How often do you look at the timestamp and log level? Wouldn't it be nice to
8
+ # see your logger output first?
9
+
10
+ # Extension of the base ruby Logger class to restore the default log
11
+ # level and timestamp formatting which is so rudely taken forcibly
12
+ # away from us by the Rails app's use of the ActiveSupport library
13
+ # that wholesale-ly modifies the Logger's format_message method.
14
+ #
15
+ class Logger < ::Logger
16
+ private
17
+ begin
18
+ # restore original log formatting to un-screw the screwage that is
19
+ # foisted upon us by the activesupport library's clean_logger.rb
20
+ alias format_message old_format_message
21
+
22
+ rescue NameError
23
+ # nothing for now -- this means we didn't need to alias since the
24
+ # method wasn't overridden
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Author < YouTubeG::Record
4
+ attr_reader :name
5
+ attr_reader :uri
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Category < YouTubeG::Record
4
+ attr_reader :label
5
+ attr_reader :term
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Contact < YouTubeG::Record
4
+ attr_reader :status
5
+ attr_reader :username
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Content < YouTubeG::Record
4
+ attr_reader :default
5
+ attr_reader :duration
6
+ attr_reader :format
7
+ attr_reader :mime_type
8
+ attr_reader :url
9
+
10
+ alias :is_default? :default
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Playlist < YouTubeG::Record
4
+ attr_reader :description
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Rating < YouTubeG::Record
4
+ attr_reader :average
5
+ attr_reader :max
6
+ attr_reader :min
7
+ attr_reader :rater_count
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Thumbnail < YouTubeG::Record
4
+ attr_reader :url
5
+ attr_reader :height
6
+ attr_reader :width
7
+ attr_reader :time
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ class YouTubeG
2
+ module Model
3
+ class User < YouTubeG::Record
4
+ attr_reader :age
5
+ attr_reader :books
6
+ attr_reader :company
7
+ attr_reader :gender
8
+ attr_reader :hobbies
9
+ attr_reader :hometown
10
+ attr_reader :location
11
+ attr_reader :movies
12
+ attr_reader :music
13
+ attr_reader :occupation
14
+ attr_reader :relationship
15
+ attr_reader :school
16
+ attr_reader :description
17
+ attr_reader :username
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,106 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Video < YouTubeG::Record
4
+ # Describes the various file formats in which a Youtube video may be
5
+ # made available and allows looking them up by format code number.
6
+ #
7
+ class Format
8
+ @@formats = Hash.new
9
+
10
+ def initialize(format_code, name)
11
+ @format_code = format_code
12
+ @name = name
13
+
14
+ @@formats[format_code] = self
15
+ end
16
+
17
+ def self.by_code(format_code)
18
+ @@formats[format_code]
19
+ end
20
+
21
+ # Flash format on YouTube site. All videos are available in this
22
+ # format.
23
+ #
24
+ FLASH = YouTubeG::Model::Video::Format.new(0, :flash)
25
+
26
+ # RTSP streaming URL for mobile video playback. H.263 video (176x144)
27
+ # and AMR audio.
28
+ #
29
+ RTSP = YouTubeG::Model::Video::Format.new(1, :rtsp)
30
+
31
+ # HTTP URL to the embeddable player (SWF) for this video. This format
32
+ # is not available for a video that is not embeddable.
33
+ #
34
+ SWF = YouTubeG::Model::Video::Format.new(5, :swf)
35
+
36
+ THREE_GPP = YouTubeG::Model::Video::Format.new(6, :three_gpp)
37
+ end
38
+
39
+ attr_reader :duration
40
+ attr_reader :noembed
41
+ attr_reader :position
42
+ attr_reader :racy
43
+ attr_reader :statistics
44
+
45
+ attr_reader :video_id
46
+ attr_reader :published_at
47
+ attr_reader :updated_at
48
+ attr_reader :categories
49
+ attr_reader :keywords
50
+ attr_reader :description
51
+ attr_reader :title
52
+ attr_reader :html_content
53
+ 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.
57
+ attr_reader :media_content
58
+
59
+ attr_reader :thumbnails # YouTubeG::Model::Thumbnail records
60
+ attr_reader :player_url
61
+ attr_reader :rating
62
+ attr_reader :view_count
63
+
64
+ # TODO:
65
+ # self atom feed
66
+ # alternate youtube watch url
67
+ # responses feed
68
+ # comments feedLink
69
+
70
+ def related
71
+ YouTubeG::Parser::VideosFeedParser.new("http://gdata.youtube.com/feeds/api/videos/#{unique_id}/related").parse
72
+ end
73
+
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
77
+ def unique_id
78
+ video_id[/videos\/([^<]+)/, 1]
79
+ end
80
+
81
+ def can_embed?
82
+ not @noembed
83
+ end
84
+
85
+ def default_media_content
86
+ @media_content.find { |c| c.is_default? }
87
+ end
88
+
89
+ def embed_html(width = 425, height = 350)
90
+ <<EDOC
91
+ <object width="#{width}" height="#{height}">
92
+ <param name="movie" value="#{embed_url}"></param>
93
+ <param name="wmode" value="transparent"></param>
94
+ <embed src="#{embed_url}" type="application/x-shockwave-flash"
95
+ wmode="transparent" width="#{width}" height="#{height}"></embed>
96
+ </object>
97
+ EDOC
98
+ end
99
+
100
+ def embed_url
101
+ @player_url.sub('watch?', '').sub('=', '/')
102
+ end
103
+
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,165 @@
1
+ require 'cgi'
2
+ require 'open-uri'
3
+ require 'rexml/document'
4
+
5
+ class YouTubeG
6
+ module Parser
7
+ class FeedParser
8
+ def initialize(url)
9
+ @url = url
10
+ end
11
+
12
+ def parse
13
+ parse_content open(@url).read
14
+ end
15
+ end
16
+
17
+ class VideoFeedParser < FeedParser
18
+
19
+ def parse_content(content)
20
+ doc = REXML::Document.new(content)
21
+ entry = doc.elements["entry"]
22
+
23
+ parse_entry(entry)
24
+ end
25
+
26
+ protected
27
+ def parse_entry(entry)
28
+ video_id = entry.elements["id"].text
29
+ published_at = Time.parse(entry.elements["published"].text)
30
+ updated_at = Time.parse(entry.elements["updated"].text)
31
+
32
+ # parse the category and keyword lists
33
+ categories = []
34
+ keywords = []
35
+ entry.elements.each("category") do |category|
36
+ # determine if it's really a category, or just a keyword
37
+ scheme = category.attributes["scheme"]
38
+ if (scheme =~ /\/categories\.cat$/)
39
+ # it's a category
40
+ categories << YouTubeG::Model::Category.new(
41
+ :term => category.attributes["term"],
42
+ :label => category.attributes["label"])
43
+
44
+ elsif (scheme =~ /\/keywords\.cat$/)
45
+ # it's a keyword
46
+ keywords << category.attributes["term"]
47
+ end
48
+ end
49
+
50
+ title = entry.elements["title"].text
51
+ html_content = entry.elements["content"].text
52
+
53
+ # parse the author
54
+ author_element = entry.elements["author"]
55
+ author = nil
56
+ if author_element
57
+ author = YouTubeG::Model::Author.new(
58
+ :name => author_element.elements["name"].text,
59
+ :uri => author_element.elements["uri"].text)
60
+ end
61
+
62
+ media_group = entry.elements["media:group"]
63
+ description = media_group.elements["media:description"].text
64
+ duration = media_group.elements["yt:duration"].attributes["seconds"].to_i
65
+
66
+ media_content = []
67
+ media_group.elements.each("media:content") do |mce|
68
+ media_content << parse_media_content(mce)
69
+ end
70
+
71
+ player_url = media_group.elements["media:player"].attributes["url"]
72
+
73
+ # parse thumbnails
74
+ thumbnails = []
75
+ media_group.elements.each("media:thumbnail") do |thumb_element|
76
+ # TODO: convert time HH:MM:ss string to seconds?
77
+ thumbnails << YouTubeG::Model::Thumbnail.new(
78
+ :url => thumb_element.attributes["url"],
79
+ :height => thumb_element.attributes["height"].to_i,
80
+ :width => thumb_element.attributes["width"].to_i,
81
+ :time => thumb_element.attributes["time"])
82
+ end
83
+
84
+ rating_element = entry.elements["gd:rating"]
85
+ rating = nil
86
+ if rating_element
87
+ rating = YouTubeG::Model::Rating.new(
88
+ :min => rating_element.attributes["min"].to_i,
89
+ :max => rating_element.attributes["max"].to_i,
90
+ :rater_count => rating_element.attributes["numRaters"].to_i,
91
+ :average => rating_element.attributes["average"].to_f)
92
+ end
93
+
94
+ view_count = (el = entry.elements["yt:statistics"]) ? el.attributes["viewCount"].to_i : 0
95
+
96
+ noembed = entry.elements["yt:noembed"] ? true : false
97
+ racy = entry.elements["yt:racy"] ? true : false
98
+
99
+ YouTubeG::Model::Video.new(
100
+ :video_id => video_id,
101
+ :published_at => published_at,
102
+ :updated_at => updated_at,
103
+ :categories => categories,
104
+ :keywords => keywords,
105
+ :title => title,
106
+ :html_content => html_content,
107
+ :author => author,
108
+ :description => description,
109
+ :duration => duration,
110
+ :media_content => media_content,
111
+ :player_url => player_url,
112
+ :thumbnails => thumbnails,
113
+ :rating => rating,
114
+ :view_count => view_count,
115
+ :noembed => noembed,
116
+ :racy => racy)
117
+ end
118
+
119
+ def parse_media_content (media_content_element)
120
+ content_url = media_content_element.attributes["url"]
121
+ format_code = media_content_element.attributes["yt:format"].to_i
122
+ format = YouTubeG::Model::Video::Format.by_code(format_code)
123
+ duration = media_content_element.attributes["duration"].to_i
124
+ mime_type = media_content_element.attributes["type"]
125
+ default = (media_content_element.attributes["isDefault"] == "true")
126
+
127
+ YouTubeG::Model::Content.new(
128
+ :url => content_url,
129
+ :format => format,
130
+ :duration => duration,
131
+ :mime_type => mime_type,
132
+ :default => default)
133
+ end
134
+ end
135
+
136
+ class VideosFeedParser < VideoFeedParser
137
+
138
+ private
139
+ def parse_content(content)
140
+ doc = REXML::Document.new(content)
141
+ feed = doc.elements["feed"]
142
+
143
+ feed_id = feed.elements["id"].text
144
+ updated_at = Time.parse(feed.elements["updated"].text)
145
+ total_result_count = feed.elements["openSearch:totalResults"].text.to_i
146
+ offset = feed.elements["openSearch:startIndex"].text.to_i
147
+ max_result_count = feed.elements["openSearch:itemsPerPage"].text.to_i
148
+
149
+ videos = []
150
+ feed.elements.each("entry") do |entry|
151
+ videos << parse_entry(entry)
152
+ end
153
+
154
+ YouTubeG::Response::VideoSearch.new(
155
+ :feed_id => feed_id,
156
+ :updated_at => updated_at,
157
+ :total_result_count => total_result_count,
158
+ :offset => offset,
159
+ :max_result_count => max_result_count,
160
+ :videos => videos)
161
+ end
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,12 @@
1
+ class YouTubeG
2
+ class Record
3
+ def initialize (params)
4
+ return if params.nil?
5
+
6
+ params.each do |key, value|
7
+ name = key.to_s
8
+ instance_variable_set("@#{name}", value) if respond_to?(name)
9
+ end
10
+ end
11
+ end
12
+ end