paperclip-youtube 2.3.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/LICENSE +26 -0
  2. data/README.md +91 -0
  3. data/Rakefile +80 -0
  4. data/generators/paperclip/USAGE +5 -0
  5. data/generators/paperclip/paperclip_generator.rb +27 -0
  6. data/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  7. data/init.rb +1 -0
  8. data/lib/generators/paperclip/USAGE +8 -0
  9. data/lib/generators/paperclip/paperclip_generator.rb +31 -0
  10. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  11. data/lib/paperclip.rb +378 -0
  12. data/lib/paperclip/attachment.rb +376 -0
  13. data/lib/paperclip/callback_compatability.rb +61 -0
  14. data/lib/paperclip/command_line.rb +86 -0
  15. data/lib/paperclip/geometry.rb +115 -0
  16. data/lib/paperclip/interpolations.rb +130 -0
  17. data/lib/paperclip/iostream.rb +45 -0
  18. data/lib/paperclip/matchers.rb +33 -0
  19. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  20. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +75 -0
  21. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  22. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  23. data/lib/paperclip/processor.rb +58 -0
  24. data/lib/paperclip/railtie.rb +24 -0
  25. data/lib/paperclip/storage.rb +3 -0
  26. data/lib/paperclip/storage/filesystem.rb +73 -0
  27. data/lib/paperclip/storage/s3.rb +192 -0
  28. data/lib/paperclip/storage/youtube.rb +331 -0
  29. data/lib/paperclip/style.rb +90 -0
  30. data/lib/paperclip/thumbnail.rb +79 -0
  31. data/lib/paperclip/upfile.rb +55 -0
  32. data/lib/paperclip/version.rb +3 -0
  33. data/lib/tasks/paperclip.rake +72 -0
  34. data/rails/init.rb +2 -0
  35. data/shoulda_macros/paperclip.rb +118 -0
  36. data/test/attachment_test.rb +921 -0
  37. data/test/command_line_test.rb +138 -0
  38. data/test/database.yml +4 -0
  39. data/test/fixtures/12k.png +0 -0
  40. data/test/fixtures/50x50.png +0 -0
  41. data/test/fixtures/5k.png +0 -0
  42. data/test/fixtures/bad.png +1 -0
  43. data/test/fixtures/s3.yml +8 -0
  44. data/test/fixtures/text.txt +0 -0
  45. data/test/fixtures/twopage.pdf +0 -0
  46. data/test/fixtures/uppercase.PNG +0 -0
  47. data/test/geometry_test.rb +177 -0
  48. data/test/helper.rb +146 -0
  49. data/test/integration_test.rb +570 -0
  50. data/test/interpolations_test.rb +143 -0
  51. data/test/iostream_test.rb +71 -0
  52. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  53. data/test/matchers/validate_attachment_content_type_matcher_test.rb +47 -0
  54. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  55. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  56. data/test/paperclip_test.rb +301 -0
  57. data/test/processor_test.rb +10 -0
  58. data/test/storage_test.rb +386 -0
  59. data/test/style_test.rb +141 -0
  60. data/test/thumbnail_test.rb +227 -0
  61. data/test/upfile_test.rb +36 -0
  62. metadata +195 -0
@@ -0,0 +1,331 @@
1
+ module Paperclip
2
+ module Interpolations
3
+ # Returns the youtube_id of the instance.
4
+ def youtube_id attachment, style
5
+ attachment.instance.youtube_id
6
+ end
7
+ end
8
+
9
+ module Storage
10
+ # Youtube video hosting, easy place to share videos for
11
+ # You can find at http://www.youtube.com/
12
+ # There are a few S3-specific options for has_attached_file:
13
+ # * +youtube_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
14
+ # to a YAML file containing the +login_name+ and +login_password+ and +youttube_username+
15
+ # and +developer_key+ that you can got it from your account on Youtube.
16
+ # You can 'environment-space' this just like you do to your
17
+ # database.yml file, so different environments can use different accounts:
18
+ # development:
19
+ # login_name: dr-click...
20
+ # login_password: 123...
21
+ # production:
22
+ # login_name: dr-click...
23
+ # login_password: 123...
24
+ #
25
+
26
+ module Youtube
27
+ def self.extended base
28
+ begin
29
+ require 'net/http'
30
+ rescue LoadError => e
31
+ log("(Error) #{e.message}")
32
+ e.message << " (Can't find required library 'net/http')"
33
+ raise e
34
+ end
35
+ begin
36
+ require 'net/https'
37
+ rescue LoadError => e
38
+ log("(Error) #{e.message}")
39
+ e.message << " (Can't find required library 'net/https')"
40
+ raise e
41
+ end
42
+ begin
43
+ require 'mime/types'
44
+ rescue LoadError => e
45
+ log("(Error) #{e.message}")
46
+ e.message << " (You may need to install the mime-types gem)"
47
+ raise e
48
+ end
49
+ begin
50
+ require 'builder'
51
+ rescue LoadError => e
52
+ log("(Error) #{e.message}")
53
+ e.message << " (You may need to install the builder gem)"
54
+ raise e
55
+ end
56
+ begin
57
+ require 'rexml/document'
58
+ rescue LoadError => e
59
+ log("(Error) #{e.message}")
60
+ e.message << " (Can't find required library 'rexml/document')"
61
+ raise e
62
+ end
63
+
64
+ base.instance_eval do
65
+ @youtube_options = @options[:youtube_options] || {}
66
+
67
+ @developer_key = @options[:developer_key] || @youtube_options[:developer_key]
68
+ @login_name = @options[:login_name] || @youtube_options[:login_name]
69
+ @login_password = @options[:login_password] || @youtube_options[:login_password]
70
+ @youttube_username = @options[:youttube_username] || @youtube_options[:youttube_username]
71
+
72
+ @auth_host = @options[:auth_host] || @youtube_options[:auth_host] || 'www.google.com'
73
+ @auth_path = @options[:auth_path] || @youtube_options[:auth_path] || '/youtube/accounts/ClientLogin'
74
+
75
+ @upload_host = @options[:upload_host] || @youtube_options[:upload_host] || 'uploads.gdata.youtube.com'
76
+ @upload_path = @options[:upload_path] || @youtube_options[:upload_path] || "/feeds/api/users/#{@youttube_username}/uploads"
77
+
78
+ @data_host = @options[:data_host] || @youtube_options[:data_host] || 'gdata.youtube.com'
79
+ end
80
+
81
+ Paperclip.interpolates(:youtube_url) do |attachment, style|
82
+ style = :thumbnail_1 if style == :thumbnail
83
+ if style == :original
84
+ 'http://www.youtube.com/watch?v=:youtube_id'
85
+ else
86
+ "http://i.ytimg.com/vi/:youtube_id/#{style.to_s.split('_').last.to_i}.jpg"
87
+ end
88
+ end
89
+ end
90
+
91
+ def login_name
92
+ @login_name
93
+ end
94
+ def login_password
95
+ @login_password
96
+ end
97
+ def developer_key
98
+ @developer_key
99
+ end
100
+ def youttube_username
101
+ @youttube_username
102
+ end
103
+ def auth_host
104
+ @auth_host
105
+ end
106
+ def auth_path
107
+ @auth_path
108
+ end
109
+ def upload_host
110
+ @upload_host
111
+ end
112
+ def upload_path
113
+ @upload_path
114
+ end
115
+ def data_host
116
+ @data_host
117
+ end
118
+ def token
119
+ @token ||= begin
120
+ http = Net::HTTP.new("www.google.com", 443)
121
+ http.use_ssl = true
122
+ body = "Email=#{YoutubeChain.esc login_name}&Passwd=#{YoutubeChain.esc login_password}&service=youtube&source=#{YoutubeChain.esc youttube_username}"
123
+ response = http.post("/youtube/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
124
+ raise response.body[/Error=(.+)/,1] if response.code.to_i != 200
125
+ @token = response.body[/Auth=(.+)/, 1]
126
+ end
127
+ end
128
+
129
+ def update_youtube_id(data)
130
+ doc = REXML::Document.new data
131
+ id = doc.root.elements["id"].text.split("/").last if doc && doc.root
132
+
133
+ if id
134
+ video = Video.find self.instance.id
135
+ video.update_attribute(:youtube_id, id)
136
+ else
137
+ log("(Error) Video has no id, please reupload to Youtube.")
138
+ raise "Video has no id, please reupload to Youtube."
139
+ end
140
+ end
141
+
142
+ def youtube_delete
143
+ http = Net::HTTP.new(data_host)
144
+ headers = {
145
+ 'Content-Type' => 'application/atom+xml',
146
+ 'Authorization' => "GoogleLogin auth=#{token}",
147
+ 'GData-Version' => '2',
148
+ 'X-GData-Key' => "key=#{developer_key}"
149
+ }
150
+
151
+ resp = http.delete(upload_path+"/#{self.instance.youtube_id}", headers)
152
+ if resp.code != "200"
153
+ log("(Error) Couldn't delete the video from Youtube")
154
+ raise "Couldn't delete the video from Youtube"
155
+ end
156
+ end
157
+
158
+ def exists?(style_name = default_style)
159
+ http = Net::HTTP.new(data_host)
160
+ headers = {
161
+ 'Content-Type' => 'application/atom+xml',
162
+ 'Authorization' => "GoogleLogin auth=#{token}",
163
+ 'GData-Version' => '2',
164
+ 'X-GData-Key' => "key=#{developer_key}"
165
+ }
166
+
167
+ resp= http.get(upload_path+"/#{self.instance.youtube_id}", headers)
168
+ if resp.code == "200"
169
+ return true
170
+ elsif
171
+ log("(Error) Couldn't find the video '#{self.instance.youtube_id}'")
172
+ return false
173
+ end
174
+ end
175
+
176
+ # Returns representation of the data of the file assigned to the given
177
+ # style, in the format most representative of the current storage.
178
+ def to_file style_name = default_style
179
+ end
180
+
181
+ def boundary
182
+ "An43094fu"
183
+ end
184
+
185
+ def request_xml(opts)
186
+ b = Builder::XmlMarkup.new
187
+ b.instruct!
188
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:media' => "http://search.yahoo.com/mrss/", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
189
+ m.tag!("media:group") do | mg |
190
+ mg.tag!("media:title", opts[:title], :type => "plain")
191
+ mg.tag!("media:description", opts[:description], :type => "plain")
192
+ mg.tag!("media:keywords", opts[:keywords].join(","))
193
+ mg.tag!('media:category', opts[:category], :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat")
194
+ mg.tag!('yt:private') if opts[:private]
195
+ end
196
+ end.to_s
197
+ end
198
+
199
+ def request_io(data, opts)
200
+ post_body = [
201
+ "--#{boundary}\r\n",
202
+ "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n",
203
+ request_xml(opts),
204
+ "\r\n--#{boundary}\r\n",
205
+ "Content-Type: #{opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n",
206
+ data,
207
+ "\r\n--#{boundary}--\r\n",
208
+ ]
209
+
210
+ YoutubeChain.new(post_body)
211
+ end
212
+
213
+ def authorization_headers
214
+ {
215
+ "Authorization" => "GoogleLogin auth=#{token}",
216
+ "X-GData-Client" => "#{youttube_username}",
217
+ "X-GData-Key" => "key=#{developer_key}"
218
+ }
219
+ end
220
+
221
+ def youtube_upload(data, video_file)
222
+ opts = { :mime_type => MIME::Types.type_for(video_file).join(),
223
+ :title => self.instance.respond_to?(:title) && !self.instance.title.blank? ? self.instance.title.blank? : video_file,
224
+ :description => self.instance.respond_to?(:description) && !self.instance.description.blank? ? self.instance.description.blank? : video_file,
225
+ :category => 'People',
226
+ :keywords => [],
227
+ :filename => video_file}
228
+
229
+ post_body_io = request_io(data, opts)
230
+ upload_headers = authorization_headers.merge({
231
+ "Slug" => "#{opts[:filename]}",
232
+ "Content-Type" => "multipart/related; boundary=#{boundary}",
233
+ "Content-Length" => "#{post_body_io.expected_length}"
234
+ })
235
+
236
+ path = upload_path
237
+
238
+ Net::HTTP.start(upload_host) do | session |
239
+ post = Net::HTTP::Post.new(path, upload_headers)
240
+ post.body_stream = post_body_io
241
+ response = session.request(post)
242
+
243
+ if response.code == "201"
244
+ update_youtube_id response.body
245
+ else
246
+ log("(Error) Couldn't upload the video to Youtube >> #{response.body}")
247
+ raise "Couldn't upload the video to Youtube >> #{response.body}"
248
+ end
249
+ end
250
+ end
251
+
252
+ def flush_writes #:nodoc:
253
+ @queued_for_write.each do |style, file|
254
+ begin
255
+ log("Uploading to youtube : #{self.instance.media_file_name}")
256
+ youtube_upload(File.open(file.path), self.instance.media_file_name)
257
+ rescue => e
258
+ log("(Error) #{e.message} - #{e.backtrace.inspect}")
259
+ raise e.message
260
+ end
261
+ end
262
+ @queued_for_write = {}
263
+ end
264
+
265
+ def flush_deletes #:nodoc:
266
+ @queued_for_delete.each do |path|
267
+ begin
268
+ log("deleting from youtube : #{self.instance.youtube_id}")
269
+ youtube_delete
270
+ rescue => e
271
+ log("(Error) #{e.message}")
272
+ raise e.message
273
+ end
274
+ end
275
+ @queued_for_delete = []
276
+ end
277
+ end
278
+
279
+
280
+ end
281
+ end
282
+
283
+
284
+ class YoutubeChain
285
+ attr_accessor :autoclose
286
+ def self.esc(s)
287
+ s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.unpack('H2'*$&.size)*'%').upcase}.tr(' ', '+')
288
+ end
289
+
290
+ def initialize(*any_ios)
291
+ @autoclose = true
292
+ @chain = any_ios.flatten.map{|e| e.respond_to?(:read) ? e : StringIO.new(e.to_s) }
293
+ end
294
+
295
+ def read(buffer_size = 1024)
296
+ current_io = @chain.shift
297
+ return false if !current_io
298
+
299
+ buf = current_io.read(buffer_size)
300
+ if !buf && @chain.empty? # End of streams
301
+ release_handle(current_io) if @autoclose
302
+ false
303
+ elsif !buf # This IO is depleted, but next one is available
304
+ release_handle(current_io) if @autoclose
305
+ read(buffer_size)
306
+ elsif buf.length < buffer_size # This IO is depleted, but we were asked for more
307
+ release_handle(current_io) if @autoclose
308
+ buf + (read(buffer_size - buf.length) || '') # and recurse
309
+ else # just return the buffer
310
+ @chain.unshift(current_io) # put the current back
311
+ buf
312
+ end
313
+ end
314
+
315
+ def expected_length
316
+ @chain.inject(0) do | len, io |
317
+ if io.respond_to?(:length)
318
+ len + (io.length - io.pos)
319
+ elsif io.is_a?(File)
320
+ len + File.size(io.path) - io.pos
321
+ else
322
+ raise "Cannot predict length of #{io.inspect}"
323
+ end
324
+ end
325
+ end
326
+
327
+ private
328
+ def release_handle(io)
329
+ io.close if io.respond_to?(:close)
330
+ end
331
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ # The Style class holds the definition of a thumbnail style, applying
4
+ # whatever processing is required to normalize the definition and delaying
5
+ # the evaluation of block parameters until useful context is available.
6
+
7
+ class Style
8
+
9
+ attr_reader :name, :attachment, :format
10
+
11
+ # Creates a Style object. +name+ is the name of the attachment,
12
+ # +definition+ is the style definition from has_attached_file, which
13
+ # can be string, array or hash
14
+ def initialize name, definition, attachment
15
+ @name = name
16
+ @attachment = attachment
17
+ if definition.is_a? Hash
18
+ @geometry = definition.delete(:geometry)
19
+ @format = definition.delete(:format)
20
+ @processors = definition.delete(:processors)
21
+ @other_args = definition
22
+ else
23
+ @geometry, @format = [definition, nil].flatten[0..1]
24
+ @other_args = {}
25
+ end
26
+ @format = nil if @format.blank?
27
+ end
28
+
29
+ # retrieves from the attachment the processors defined in the has_attached_file call
30
+ # (which method (in the attachment) will call any supplied procs)
31
+ # There is an important change of interface here: a style rule can set its own processors
32
+ # by default we behave as before, though.
33
+ def processors
34
+ @processors || attachment.processors
35
+ end
36
+
37
+ # retrieves from the attachment the whiny setting
38
+ def whiny
39
+ attachment.whiny
40
+ end
41
+
42
+ # returns true if we're inclined to grumble
43
+ def whiny?
44
+ !!whiny
45
+ end
46
+
47
+ def convert_options
48
+ attachment.send(:extra_options_for, name)
49
+ end
50
+
51
+ # returns the geometry string for this style
52
+ # if a proc has been supplied, we call it here
53
+ def geometry
54
+ @geometry.respond_to?(:call) ? @geometry.call(attachment.instance) : @geometry
55
+ end
56
+
57
+ # Supplies the hash of options that processors expect to receive as their second argument
58
+ # Arguments other than the standard geometry, format etc are just passed through from
59
+ # initialization and any procs are called here, just before post-processing.
60
+ def processor_options
61
+ args = {}
62
+ @other_args.each do |k,v|
63
+ args[k] = v.respond_to?(:call) ? v.call(attachment) : v
64
+ end
65
+ [:processors, :geometry, :format, :whiny, :convert_options].each do |k|
66
+ (arg = send(k)) && args[k] = arg
67
+ end
68
+ args
69
+ end
70
+
71
+ # Supports getting and setting style properties with hash notation to ensure backwards-compatibility
72
+ # eg. @attachment.styles[:large][:geometry]@ will still work
73
+ def [](key)
74
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
75
+ send(key)
76
+ elsif defined? @other_args[key]
77
+ @other_args[key]
78
+ end
79
+ end
80
+
81
+ def []=(key, value)
82
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
83
+ send("#{key}=".intern, value)
84
+ else
85
+ @other_args[key] = value
86
+ end
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,79 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail < Processor
4
+
5
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options, :source_file_options
6
+
7
+ # Creates a Thumbnail object set to work on the +file+ given. It
8
+ # will attempt to transform the image into one defined by +target_geometry+
9
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
+ # unless specified. Thumbnail creation will raise no errors unless
11
+ # +whiny+ is true (which it is, by default. If +convert_options+ is
12
+ # set, the options will be appended to the convert command upon image conversion
13
+ def initialize file, options = {}, attachment = nil
14
+ super
15
+
16
+ geometry = options[:geometry]
17
+ @file = file
18
+ @crop = geometry[-1,1] == '#'
19
+ @target_geometry = Geometry.parse geometry
20
+ @current_geometry = Geometry.from_file @file
21
+ @source_file_options = options[:source_file_options]
22
+ @convert_options = options[:convert_options]
23
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
24
+ @format = options[:format]
25
+
26
+ @source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
27
+ @convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
28
+
29
+ @current_format = File.extname(@file.path)
30
+ @basename = File.basename(@file.path, @current_format)
31
+
32
+ end
33
+
34
+ # Returns true if the +target_geometry+ is meant to crop.
35
+ def crop?
36
+ @crop
37
+ end
38
+
39
+ # Returns true if the image is meant to make use of additional convert options.
40
+ def convert_options?
41
+ !@convert_options.nil? && !@convert_options.empty?
42
+ end
43
+
44
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
45
+ # that contains the new image.
46
+ def make
47
+ src = @file
48
+ dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
49
+ dst.binmode
50
+
51
+ begin
52
+ parameters = []
53
+ parameters << source_file_options
54
+ parameters << ":source"
55
+ parameters << transformation_command
56
+ parameters << convert_options
57
+ parameters << ":dest"
58
+
59
+ parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
60
+
61
+ success = Paperclip.run("convert", parameters, :source => "#{File.expand_path(src.path)}[0]", :dest => File.expand_path(dst.path))
62
+ rescue PaperclipCommandLineError => e
63
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
64
+ end
65
+
66
+ dst
67
+ end
68
+
69
+ # Returns the command ImageMagick's +convert+ needs to transform the image
70
+ # into the thumbnail.
71
+ def transformation_command
72
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
73
+ trans = []
74
+ trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
75
+ trans << "-crop" << %["#{crop}"] << "+repage" if crop
76
+ trans
77
+ end
78
+ end
79
+ end