heywatch 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Manifest.txt +18 -0
- data/README.txt +163 -0
- data/Rakefile +74 -0
- data/lib/heywatch.rb +88 -0
- data/lib/heywatch/account.rb +32 -0
- data/lib/heywatch/auth.rb +83 -0
- data/lib/heywatch/base.rb +269 -0
- data/lib/heywatch/browser.rb +98 -0
- data/lib/heywatch/discover.rb +39 -0
- data/lib/heywatch/download.rb +30 -0
- data/lib/heywatch/encoded_video.rb +49 -0
- data/lib/heywatch/ext.rb +118 -0
- data/lib/heywatch/job.rb +32 -0
- data/lib/heywatch/version.rb +9 -0
- data/lib/heywatch/video.rb +25 -0
- data/setup.rb +1585 -0
- data/test/test_helper.rb +8 -0
- data/test/test_heywatch.rb +149 -0
- metadata +80 -0
@@ -0,0 +1,269 @@
|
|
1
|
+
module HeyWatch
|
2
|
+
# You can connect to the Hey!Watch service permanently
|
3
|
+
#
|
4
|
+
# Base::establish_connection! :login => "login", :password => "password"
|
5
|
+
#
|
6
|
+
# You can access all the resources and manipulate the objects like in ActiveRecord
|
7
|
+
#
|
8
|
+
# EncodedVideo.find(:all, :conditions => {:title => /bad video/i}).each do |v|
|
9
|
+
# v.destroy
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# Format.create :name => "my new format", :video_codec => "mpeg4", ...
|
13
|
+
#
|
14
|
+
# f = Format.new
|
15
|
+
# f.container = "avi"
|
16
|
+
# f.audio_codec = "mp3"
|
17
|
+
# ...
|
18
|
+
# f.save
|
19
|
+
#
|
20
|
+
# v = Video.find(15).update_attributes :title => "My edited title"
|
21
|
+
# puts v.title
|
22
|
+
#
|
23
|
+
# Format.find_by_name("iPod 4:3")
|
24
|
+
#
|
25
|
+
# Format.find_all_by_name /ipod/i
|
26
|
+
class Base
|
27
|
+
attr_reader :attributes, :errors
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def session=(session) #:nodoc:
|
31
|
+
@session = session
|
32
|
+
end
|
33
|
+
|
34
|
+
def session #:nodoc:
|
35
|
+
(@session || Base.session) rescue nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Establish the connection for all your session
|
39
|
+
def establish_connection!(options={})
|
40
|
+
Base.session = Browser::login(options[:login], options[:password])
|
41
|
+
end
|
42
|
+
|
43
|
+
def disconnect!
|
44
|
+
Base.session = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def method_missing(m, *args) #:nodoc:
|
48
|
+
if m.to_s =~ /find_by_(.*)/
|
49
|
+
find(:first, :conditions => {$1.to_sym => args.first}) rescue nil
|
50
|
+
elsif m.to_s =~ /find_all_by_(.*)/
|
51
|
+
find :all, {:conditions => {$1.to_sym => args.first}}.merge(args[1]||{})
|
52
|
+
else
|
53
|
+
raise NoMethodError, "undefined method `#{m.to_s}' for #{self}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def path #:nodoc:
|
58
|
+
return @path if @path
|
59
|
+
"/"+self.to_s.split("::").last.
|
60
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
61
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
62
|
+
tr("-", "_").
|
63
|
+
downcase
|
64
|
+
end
|
65
|
+
|
66
|
+
# if the path of the resource is not standard, you can set it manually.
|
67
|
+
#
|
68
|
+
# class Log < Base
|
69
|
+
# self.path = "/user_logs"
|
70
|
+
# end
|
71
|
+
def path=(path) #:nodoc:
|
72
|
+
@path = path
|
73
|
+
end
|
74
|
+
|
75
|
+
# Find objects
|
76
|
+
#
|
77
|
+
# Arguments:
|
78
|
+
#
|
79
|
+
# * <tt>ID</tt> ID of the object
|
80
|
+
# * <tt>:first</tt> retrieve the first result
|
81
|
+
# * <tt>:all</tt> retrieve all the results
|
82
|
+
#
|
83
|
+
# Options:
|
84
|
+
#
|
85
|
+
# * <tt>:conditions</tt> {:fieldname => "keyword"} or {:fieldname => /[0-9]+/}
|
86
|
+
# * <tt>:order</tt> to sort the result
|
87
|
+
# * <tt>:limit</tt> limit the number of result to return
|
88
|
+
# * <tt>:include</tt> fetch the object to include
|
89
|
+
#
|
90
|
+
# Format.find(:all, :conditions => {:name => /mobile/i})
|
91
|
+
# Format.find(:all, :conditions => {:width => '<320'})
|
92
|
+
# Job.find(:all, :include => "encoded_video", :order => "created_at DESC", :limit => 3)
|
93
|
+
# Video.find :first
|
94
|
+
# Download.find(5)
|
95
|
+
def find(*args)
|
96
|
+
scope, options = args
|
97
|
+
options ||= {}
|
98
|
+
case scope
|
99
|
+
when :all then find_every(options)
|
100
|
+
when :first then find_every(options).first
|
101
|
+
else find_single(scope, options)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def find_every(options) #:nodoc:
|
108
|
+
collection = []
|
109
|
+
res = HeyWatch::response(Browser::get(path, session).body)
|
110
|
+
return collection if res.empty?
|
111
|
+
|
112
|
+
[res[res.keys.first]].flatten.each {|object| collection << new(object)}
|
113
|
+
collection.find_with_options(options)
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_single(arg, options) #:nodoc:
|
117
|
+
new(HeyWatch::response(Browser::get(path+"/"+arg.to_s, session).body)).include_heywatch_object(options[:include])
|
118
|
+
end
|
119
|
+
|
120
|
+
public
|
121
|
+
|
122
|
+
# Create the object
|
123
|
+
#
|
124
|
+
# Download.create :url => "http://host.com/video.avi"
|
125
|
+
def create(attributes={})
|
126
|
+
new(HeyWatch::response(Browser::post(path, attributes, session).body))
|
127
|
+
end
|
128
|
+
|
129
|
+
# Update the object passing its ID
|
130
|
+
#
|
131
|
+
# EncodedVideo.update 15, :title => "my title"
|
132
|
+
def update(id, attributes={})
|
133
|
+
Browser::put(path+"/"+id.to_s, attributes, session)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Destroy the object passing its ID
|
137
|
+
#
|
138
|
+
# Format.destroy 12
|
139
|
+
def destroy(id)
|
140
|
+
Browser::delete(path+"/"+id.to_s, session)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Destroy all the objects
|
144
|
+
#
|
145
|
+
# Video.destroy_all
|
146
|
+
def destroy_all
|
147
|
+
find(:all).each do |object|
|
148
|
+
object.destroy
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Count request
|
153
|
+
#
|
154
|
+
# Accept :conditions like in Base#find
|
155
|
+
def count(field=nil, options={})
|
156
|
+
find(:all, options).size
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Instanciate a new object
|
161
|
+
#
|
162
|
+
# Format.new :name => "test format", :sample_rate => 24000
|
163
|
+
def initialize(attributes={})
|
164
|
+
@attributes = attributes.underscore_keys!
|
165
|
+
@attributes.type_cast!
|
166
|
+
end
|
167
|
+
|
168
|
+
def include_heywatch_object(objects=nil) #:nodoc:
|
169
|
+
return self if objects.nil?
|
170
|
+
objects = objects.to_s.split(",") if objects.is_a?(String) or objects.is_a?(Symbol)
|
171
|
+
objects.each do |ob|
|
172
|
+
begin
|
173
|
+
self.instance_variable_set "@#{ob}", (self.send(ob) rescue nil)
|
174
|
+
self.instance_eval "attr_reader #{ob.to_sym}"
|
175
|
+
rescue
|
176
|
+
end
|
177
|
+
end
|
178
|
+
self
|
179
|
+
end
|
180
|
+
|
181
|
+
# Save the object.
|
182
|
+
#
|
183
|
+
# If the object doesn't exist, it will be created, otherwise updated.
|
184
|
+
# If an error occurred, @errors will be filled. This method doesn't raise.
|
185
|
+
def save
|
186
|
+
begin
|
187
|
+
save!
|
188
|
+
true
|
189
|
+
rescue => e
|
190
|
+
@errors = e.to_s
|
191
|
+
false
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Save the current object
|
196
|
+
#
|
197
|
+
# Raise if an error occurred. Return self.
|
198
|
+
def save!
|
199
|
+
if new_record?
|
200
|
+
self.class.create(@attributes)
|
201
|
+
else
|
202
|
+
update_attributes(@attributes)
|
203
|
+
end
|
204
|
+
self
|
205
|
+
end
|
206
|
+
|
207
|
+
def id #:nodoc:
|
208
|
+
@attributes["id"].to_i
|
209
|
+
end
|
210
|
+
|
211
|
+
# Update the object with the given attributes
|
212
|
+
#
|
213
|
+
# Video.find(10).update_attributes :title => "test title"
|
214
|
+
def update_attributes(attributes={})
|
215
|
+
if self.class.update(id, attributes)
|
216
|
+
reload
|
217
|
+
true
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Destroy the object
|
222
|
+
#
|
223
|
+
# Video.find(56).destroy
|
224
|
+
def destroy
|
225
|
+
self.class.destroy(id)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Reload the current object
|
229
|
+
#
|
230
|
+
# j = Job.find(5400)
|
231
|
+
# j.reload
|
232
|
+
def reload
|
233
|
+
unless new_record?
|
234
|
+
@attributes = self.class.find(self.id).attributes
|
235
|
+
end
|
236
|
+
self
|
237
|
+
end
|
238
|
+
|
239
|
+
def new_record? #:nodoc:
|
240
|
+
@attributes["id"].nil?
|
241
|
+
end
|
242
|
+
|
243
|
+
def method_missing(m, *args) #:nodoc:
|
244
|
+
method_name = m.to_s
|
245
|
+
case method_name[-1..-1]
|
246
|
+
when '='
|
247
|
+
@attributes[method_name[0..-2]] = *args.first
|
248
|
+
when '?'
|
249
|
+
@attributes[method_name[0..-2]]
|
250
|
+
else
|
251
|
+
if instance_variables.include?("@#{m.to_s}")
|
252
|
+
eval("@#{m.to_s}")
|
253
|
+
else
|
254
|
+
if object_id = @attributes[m.to_s+"_id"] # belongs_to
|
255
|
+
klass = HeyWatch::const_get(m.to_s.camelize)
|
256
|
+
klass.session = self.class.session
|
257
|
+
klass.find(object_id)
|
258
|
+
else
|
259
|
+
@attributes[m.to_s] rescue nil
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
Resources.each do |k|
|
267
|
+
HeyWatch.module_eval(%{class #{k.to_s.camelize} < Base; end})
|
268
|
+
end
|
269
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require "net/http"
|
2
|
+
|
3
|
+
module HeyWatch
|
4
|
+
# This class is used to request the Hey!Watch service.
|
5
|
+
#
|
6
|
+
# Browser::get '/video', session
|
7
|
+
# Browser::post '/download', {:url => 'http://host.com/video.avi'}, session
|
8
|
+
# Browser::put '/encoded_video/54000', {:title => 'new title'}, session
|
9
|
+
# Browser::delete '/encoded_video/54000', session
|
10
|
+
class Browser
|
11
|
+
class << self
|
12
|
+
# Raise when code response != 2xx
|
13
|
+
def raise_if_response_error(res)
|
14
|
+
code = res.response.code.to_i
|
15
|
+
message = res.response.message
|
16
|
+
return if code.to_s =~ /^2/
|
17
|
+
|
18
|
+
raise RequestError, HeyWatch::response(res.body).content if code == 400
|
19
|
+
raise NotAuthorized, message if code == 401
|
20
|
+
raise ResourceNotFound, message if code == 404
|
21
|
+
raise ServerError, message if code == 500
|
22
|
+
end
|
23
|
+
|
24
|
+
def header(session=nil) #:nodoc:
|
25
|
+
h = {}
|
26
|
+
h.merge!({"Cookie" => "_session_id=#{session};"}) if session
|
27
|
+
h.merge!({"User-Agent" => "Hey!Watch ruby API - #{VERSION::STRING}"})
|
28
|
+
end
|
29
|
+
|
30
|
+
# Login to Hey!Watch service. Return the session ID.
|
31
|
+
#
|
32
|
+
# You should not use it directly, use Auth#create instead
|
33
|
+
#
|
34
|
+
# Browser::login 'login', 'password'
|
35
|
+
def login(login, password) #:nodoc:
|
36
|
+
res = Browser::post("/auth/login", :login => login, :password => password)
|
37
|
+
return res["Set-cookie"].match(/_session_id=(.*);/i)[1].to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
# GET on path
|
41
|
+
def get(path, session=nil)
|
42
|
+
path += ".#{OutFormat}" unless path.include? "."
|
43
|
+
res = Net::HTTP.start(Host) {|http| http.get(path, header(session))}
|
44
|
+
raise_if_response_error(res)
|
45
|
+
res
|
46
|
+
end
|
47
|
+
|
48
|
+
# POST on path and pass the query(Hash)
|
49
|
+
def post(path, query={}, session=nil)
|
50
|
+
res = Net::HTTP.start(Host) {|http| http.post(path, query.merge(:format => OutFormat).to_a.map{|x| x.join("=")}.join("&"), self.header(session))}
|
51
|
+
raise_if_response_error(res)
|
52
|
+
res
|
53
|
+
end
|
54
|
+
|
55
|
+
# PUT on path and pass the query(Hash)
|
56
|
+
def put(path, query={}, session=nil)
|
57
|
+
req = Net::HTTP::Put.new(path, header(session))
|
58
|
+
req.form_data = query.merge(:format => OutFormat)
|
59
|
+
res = Net::HTTP.new(Host).start {|http| http.request(req) }
|
60
|
+
raise_if_response_error(res)
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
# DELETE on path
|
65
|
+
def delete(path, session=nil)
|
66
|
+
res = Net::HTTP.start(Host) {|http| http.delete(path+"."+OutFormat, header(session))}
|
67
|
+
raise_if_response_error(res)
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def post_multipart(path, attributes={}, session=nil) #:nodoc:
|
72
|
+
file = attributes.delete(:file)
|
73
|
+
params = [file_to_multipart("data", File.basename(file),"application/octet-stream", File.read(file))]
|
74
|
+
attributes.merge("format" => OutFormat).each_pair{|k,v| params << text_to_multipart(k.to_s, v.to_s)}
|
75
|
+
|
76
|
+
boundary = '349832898984244898448024464570528145'
|
77
|
+
query = params.collect {|p| '--' + boundary + "\r\n" + p}.join('') + "--" + boundary + "--\r\n"
|
78
|
+
res = Net::HTTP.start(Host) {|http| http.post(path, query, header(session).merge("Content-Type" => "multipart/form-data; boundary=" + boundary))}
|
79
|
+
raise_if_response_error(res)
|
80
|
+
res
|
81
|
+
end
|
82
|
+
|
83
|
+
def text_to_multipart(key,value) #:nodoc:
|
84
|
+
return "Content-Disposition: form-data; name=\"#{CGI::escape(key.to_s)}\"\r\n" +
|
85
|
+
"\r\n" +
|
86
|
+
"#{value}\r\n"
|
87
|
+
end
|
88
|
+
|
89
|
+
def file_to_multipart(key,filename,mime_type,content) #:nodoc:
|
90
|
+
return "Content-Disposition: form-data; name=\"#{CGI::escape(key.to_s)}\"; filename=\"#{filename}\"\r\n" +
|
91
|
+
"Content-Transfer-Encoding: binary\r\n" +
|
92
|
+
"Content-Type: #{mime_type}\r\n" +
|
93
|
+
"\r\n" +
|
94
|
+
"#{content}\r\n"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module HeyWatch
|
2
|
+
class VideoNotFound < RuntimeError; end
|
3
|
+
class MultipleVideoFound < RuntimeError; end
|
4
|
+
|
5
|
+
class Discover < Base
|
6
|
+
# Discover video from the given URL
|
7
|
+
#
|
8
|
+
# If you pass a block, you will follow the process until you get the raw video.
|
9
|
+
# Return the new video if pass a block.
|
10
|
+
#
|
11
|
+
# If more than 1 video is found, raise an exception.
|
12
|
+
#
|
13
|
+
# Discover.create(:url => 'http://host.com/', :download => true) {|percent, total_size, received| ...}
|
14
|
+
def self.create(attributes={}, &block)
|
15
|
+
discover = super
|
16
|
+
return discover unless attributes[:download] and block_given?
|
17
|
+
|
18
|
+
while (discover.status rescue "working") == "working"
|
19
|
+
discover.reload rescue nil
|
20
|
+
end
|
21
|
+
|
22
|
+
case discover.status
|
23
|
+
when "ok"
|
24
|
+
if discover.results.result.is_a?(String)
|
25
|
+
while discover.download_id.nil?
|
26
|
+
sleep 1
|
27
|
+
discover.reload
|
28
|
+
end
|
29
|
+
video = Download.progress(discover.download, &block)
|
30
|
+
return video
|
31
|
+
else
|
32
|
+
raise MultipleVideoFound, "You have to choose the video you want to transfer"
|
33
|
+
end
|
34
|
+
when "error"
|
35
|
+
raise VideoNotFound, "No video found in this website"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module HeyWatch
|
2
|
+
class DownloadFailed < RuntimeError; end
|
3
|
+
|
4
|
+
class Download < Base
|
5
|
+
# Download file from the given URL
|
6
|
+
#
|
7
|
+
# If you pass a block, you get the progress. Return the new video.
|
8
|
+
#
|
9
|
+
# Download.create(:url => 'http://host.com/file.avi') {|percent, total_size, received| ...}
|
10
|
+
def self.create(attributes={}, &block)
|
11
|
+
download = super
|
12
|
+
return download unless block_given?
|
13
|
+
self.progress(download, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.progress(download, &block)
|
17
|
+
while download.status == "downloading"
|
18
|
+
download.reload
|
19
|
+
yield download.progress.percent, download.length, download.progress.current_length
|
20
|
+
sleep 1
|
21
|
+
end
|
22
|
+
case download.status
|
23
|
+
when "finished"
|
24
|
+
return download.video
|
25
|
+
when "error"
|
26
|
+
raise DownloadFailed, download.error_msg
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module HeyWatch
|
2
|
+
class EncodedVideo < Base
|
3
|
+
def url
|
4
|
+
res = Browser::get("#{self.class.path}/#{self.id}.bin", self.class.session)
|
5
|
+
HeyWatch::sanitize_url(res.get_fields("Location").first)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Generate a thumbnail of the encoded video
|
9
|
+
#
|
10
|
+
# options:
|
11
|
+
# * <tt>start:</tt> offset in second
|
12
|
+
# * <tt>width</tt>
|
13
|
+
# * <tt>height</tt>
|
14
|
+
#
|
15
|
+
# All these options are optionals. Return binary data.
|
16
|
+
#
|
17
|
+
# EncodedVideo.find(:first).thumbnail :start => 15, :width => 320, :height => 240
|
18
|
+
def thumbnail(options={})
|
19
|
+
param = options.to_a.map{|v| v.join("=")}.join("&")
|
20
|
+
res = Browser::get("#{self.class.path}/#{self.id}.jpg#{'?'+param unless param.empty?}", self.class.session)
|
21
|
+
case res
|
22
|
+
when Net::HTTPRedirection
|
23
|
+
Net::HTTP.get_response(URI(HeyWatch::sanitize_url(res["location"]))).body
|
24
|
+
else
|
25
|
+
res.body
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Download the encoded video
|
30
|
+
#
|
31
|
+
# The path is . by default. You can pass a block to have progression. Return the full path of the video.
|
32
|
+
#
|
33
|
+
# EncodedVideo.find(:first).download('/tmp') {|progress| puts progress.to_s + '%'}
|
34
|
+
def download(path=".")
|
35
|
+
uri = URI(url)
|
36
|
+
size = self.specs["size"] * 1024
|
37
|
+
path = File.join(path, self.filename)
|
38
|
+
file = File.open(path, "wb")
|
39
|
+
Net::HTTP.start(uri.host) do |http|
|
40
|
+
http.get(uri.path) do |data|
|
41
|
+
file.write data
|
42
|
+
yield((File.size(path) / size.to_f) * 100) if block_given?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
file.close
|
46
|
+
return path
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|