lib/tumblr/post.rb in tumblr-rb-1.3.0 vs lib/tumblr/post.rb in tumblr-rb-2.0.0.alpha

- old
+ new

@@ -1,190 +1,272 @@ -# An object that represents a post. From: -# http://www.tumblr.com/docs/en/api#api_write -class Tumblr +module Tumblr + # A Tumblr::Post object can be serialized into a YAML front-matter formatted string, + # and provides convenient ways to publish, edit, and delete to the API. + # Don't call #new directly, instead use Post::create to instantiate a subclass. class Post - BASIC_PARAMS = [:date,:tags,:format,:group,:generator,:private, - :slug,:state,:'send-to-twitter',:'publish-on',:'reblog-key'] - POST_PARAMS = [:title,:body,:source,:caption,:'click-through-url', - :quote,:name,:url,:description,:conversation, - :embed,:'externally-hosted-url'] - REBLOG_PARAMS = [:comment, :as] - - def self.parameters(*attributes) - if !attributes.blank? - @parameters = attributes - attr_accessor *@parameters - end - @parameters + + autoload :Text, 'tumblr/post/text' + autoload :Quote, 'tumblr/post/quote' + autoload :Link, 'tumblr/post/link' + autoload :Answer, 'tumblr/post/answer' + autoload :Video, 'tumblr/post/video' + autoload :Audio, 'tumblr/post/audio' + autoload :Photo, 'tumblr/post/photo' + autoload :Chat, 'tumblr/post/chat' + + FIELDS = [ + :blog_name, :id, :post_url, :type, :timestamp, :date, :format, + :reblog_key, :tags, :bookmarklet, :mobile, :source_url, :source_title, + :total_posts, + :photos, :dialogue, :player # Post-specific response fields + ] + + # Some post types have several "body keys", which allow the YAML front-matter + # serialization to seem a bit more human. This separator separates those keys. + POST_BODY_SEPARATOR = "\n\n" + + # Given a Request, perform it and transform the response into a list of Post objects. + def self.perform(request) + response = request.perform + posts = response.parse["response"]["posts"] + + (posts || []).map{|post| self.create(post) } end - - attr_reader :type, :state, :post_id, :format - attr_accessor :slug, :date, :group, :generator, :reblog_key - - def initialize(post_id = nil) - @post_id = post_id if post_id + + # Insantiate a subclass of Tumblr::Post, corresponding to the post's type. + def self.create(post_response) + type = post_response["type"].to_s.capitalize.to_sym + get_post_type(post_response["type"]).new(post_response) end - - def private=(bool) - @private = bool ? true : false + + # Get a subclass of Tumblr::Post based on a type token. + def self.get_post_type(type) + const_get type.to_s.capitalize.to_sym end - - def private? - @private + + # Transform a yaml front matter formatted String into a subclass of Tumblr::Post + def self.load(doc) + create parse(doc) end - - def tags(*post_tags) - @tags = post_tags.join(',') if !post_tags.blank? - @tags + + # Load a document and transform into a post via file path + def self.load_from_path(path) + raise ArgumentError, "Given path: #{path} is not a file" unless File.file? File.expand_path(path) + post_type = infer_post_type_from_extname File.extname(path) + if post_type == :text + load File.read(File.expand_path(path)) + else + load_from_binary File.new(File.expand_path(path), "rb"), post_type + end end - - def state=(published_state) - allowed_states = [:published, :draft, :submission, :queue] - if !allowed_states.include?(published_state.to_sym) - raise "Not a recognized published state. Must be one of #{allowed_states.inspect}" + + def self.load_from_binary(file, post_type = nil) + file_size_in_mb = File.size(file.path).to_f / 2**20 + raise ArgumentError, "File size is greater than 5 MB (Tumblr's limit)" if file_size_in_mb > 5 + post_type ||= infer_post_type_from_extname File.extname(file.path) + get_post_type(post_type).new "data" => file.read + end + + # Transform a yaml front matter formatted String into a set of parameters to create a post. + def self.parse(doc) + doc =~ /^(\s*---(.*?)---\s*)/m + + if Regexp.last_match + meta_data = YAML.load(Regexp.last_match[2].strip) + doc_body = doc.sub(Regexp.last_match[1],'').strip + else + meta_data = {} + doc_body = doc end - @state = published_state.to_sym + meta_data["type"] ||= infer_post_type_from_string(doc_body) + meta_data["format"] ||= "markdown" + + post_type = get_post_type(meta_data["type"]) + post_body_parts = doc_body.split(POST_BODY_SEPARATOR) + + pairs = pair_post_body_types(post_type.post_body_keys, post_body_parts.dup) + Hash[pairs].merge(meta_data) end - - def format=(markup) - markup_format = markup.to_sym - if markup_format.eql?(:html) || markup_format.eql?(:markdown) - @format = markup_format + + # Pair the post body keys for a particular post type with a list of values. + # If the length list of values is greater than the list of keys, the last key + # should be paired with the remaining values joined together. + def self.pair_post_body_types(keys, values) + values.fill(keys.length - 1) do |i| + values[keys.length - 1, values.length].join(POST_BODY_SEPARATOR) end + keys.map(&:to_s).zip values end - - def send_to_twitter(status=false) - if status - if status.to_sym.eql?(:no) - @send_to_twitter = false - else - @send_to_twitter = status - end + + def self.infer_post_type_from_extname(extname) + require 'rack' + mime_type = Rack::Mime.mime_type extname + case mime_type.split("/").first + when "image" + :photo + when "video" + :video + when "audio" + :audio + else + :text end - @send_to_twitter end - - def publish_on(pubdate=nil) - @publish_on = pubdate if state.eql?(:queue) && pubdate - @publish_on + + def self.infer_post_type_from_string(str) + require 'uri' + video_hosts = ["youtube.com", "vimeo.com", "youtu.be"] + audio_hosts = ["open.spotify.com", "soundcloud.com", "snd.sc"] + url = URI.parse(str) + if url.is_a?(URI::HTTP) + return :video if video_hosts.find {|h| url.host.include?(h) } + return :audio if audio_hosts.find {|h| url.host.include?(h) } + :link + elsif url.scheme.eql?("spotify") + :audio + else + :text + end + rescue URI::InvalidURIError + :text end - - # Convert to a hash to be used in post writing/editing - def to_h - post_hash = {} - basics = [:post_id, :type, :date, :tags, :format, :group, :generator, - :slug, :state, :send_to_twitter, :publish_on, :reblog_key] - params = basics.select {|opt| respond_to?(opt) && send(opt) } - params |= self.class.parameters.select {|opt| send(opt) } unless self.class.parameters.blank? - params.each { |key| post_hash[key.to_s.gsub('_','-').to_sym] = send(key) } unless params.empty? - post_hash[:private] = 1 if private? - post_hash + + # A post_body_key determines what parts of the serialization map to certain + # fields in the post request. + def self.post_body_keys + [:body] end - - # Publish this post to Tumblr - def write(email, password) - Writer.new(email,password).write(to_h) + + # Serialize a post. + def self.dump(post) + post.serialize end - - def edit(email, password) - Writer.new(email,password).edit(to_h) + + def initialize(post_response = {}) + post_response.delete_if {|k,v| !(FIELDS | Tumblr::Client::POST_OPTIONS).map(&:to_s).include? k.to_s } + post_response.each_pair do |k,v| + instance_variable_set "@#{k}".to_sym, v + end end - - def reblog(email, password) - Writer.new(email,password).reblog(to_h) + + # Transform this post into it's YAML front-matter post form. + def serialize + buffer = YAML.dump(meta_data) + buffer << "---\x0D\x0A" + buffer << post_body + buffer end - - def delete(email, password) - Writer.new(email,password).delete(to_h) + + # Given a client, publish this post to tumblr. + def post(client) + client.post(request_parameters) end - - def like(email,password) - if (post_id && reblog_key) - Reader.new(email,password).like(:'post-id' => post_id, :'reblog-key' => reblog_key) - end + + # Given a client, edit this post. + def edit(client) + raise "Must have an id to edit a post" unless id + client.edit(request_parameters) end - - def unlike(email,password) - if (post_id && reblog_key) - Reader.new(email,password).unlike(:'post-id' => post_id, :'reblog-key' => reblog_key) - end + + # Given a client, delete this post. + def delete(client) + raise "Must have an id to delete a post" unless id + client.delete(:id => id) end - - # Write to Tumblr and set state to Publish - def publish_now(email, password) - self.state = :published - return edit(email,password) if post_id - write(email,password) + + # Transform this Post into a hash ready to be serialized and posted to the API. + # This looks for the fields of Tumblr::Client::POST_OPTIONS as methods on the object. + def request_parameters + Hash[(Tumblr::Client::POST_OPTIONS | [:id, :type]).map {|key| + [key.to_s, send(key)] if respond_to?(key) && send(key) + }] end - - # Save as a draft - def save_as_draft(email, password) - self.state = :draft - return edit(email,password) if post_id - write(email,password) + + # Which parts of this post represent it's meta data (eg. they're not part of the body). + def meta_data + request_parameters.reject {|k,v| self.class.post_body_keys.include?(k.to_sym) } end - - # Adds to Queue. Pass an additional date to publish at a specific date. - def add_to_queue(email, password, pubdate = nil) - self.state = :queue - self.publish_on(pubdate) if pubdate - return edit(email,password) if post_id - write(email,password) + + # Below this line are public methods that are used to transform this post into an API request. + + def id + @id.to_i unless @id.nil? end - - # Convert post to a YAML representation - def to_yaml - post = {} - post['data'] = post_data - post['body'] = to_h[post_body].to_s - YAML.dump(post) + + def type + @type.to_s end - - # Convert post to a string for writing to a file - def to_s - post_string = YAML.dump(post_data) - post_string += "---\x0D\x0A" - post_string += YAML.load(to_yaml)['body'] - post_string + + def reblog_key + @reblog_key end - - private - - def post_data - data = {} - to_h.each_pair do |key,value| - data[key.to_s] = value.to_s + + def state + @state + end + + def tags + if @tags.respond_to? :join + @tags.join(",") + else + @tags end - data.reject! {|key,value| key.eql?(post_body.to_s) } - data end - + + def tweet + @tweet + end + + def date + @date + end + + def format + @format + end + + def slug + @slug + end + + # These are handy convenience methods. + + def markdown? + @format.to_s == "markdown" + end + + def published? + @state.to_s == "published" + end + + def draft? + @state.to_s == "draft" + end + + def queued? + @state.to_s == "queued" or @state.to_s == "queue" + end + + def private? + @state.to_s == "private" + end + + def publish! + @state = "published" + end + + def queue! + @state = "queue" + end + + def draft! + @state ="draft" + end + + private + def post_body - case type - when :regular - :body - when :photo - :source - when :quote - :quote - when :link - :url - when :conversation - :conversation - when :video - :embed - when :audio - :'externally-hosted-url' - else - raise "#{type} is not a recognized Tumblr post type." - end + self.class.post_body_keys.map{|key| self.send(key) }.join(POST_BODY_SEPARATOR) end + end end - -require 'tumblr/post/regular' -require 'tumblr/post/photo' -require 'tumblr/post/quote' -require 'tumblr/post/link' -require 'tumblr/post/conversation' -require 'tumblr/post/video' -require 'tumblr/post/audio' \ No newline at end of file