require 'time' class PostMetadataFetcher TIMESTAMP_PATTERN = Regexp.new( '\A(\d{4})-(\d{2})-(\d{2})T(\d{2})' \ '(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?' \ '(Z|[+-]\d{2}:?\d{2})?\z') def self.fetch raw_url uri, = Preview::UrlSafety.validate(raw_url) response = Preview::HttpFetcher.fetch( uri.to_s, max_bytes: Preview::ThumbnailFetcher::HTML_MAX_BYTES) metadata = Preview::HtmlMetadataExtractor.extract(response) document = Nokogiri::HTML.parse(response.body) content = lambda { |name| document .at_css("meta[property='#{ name }'], meta[name='#{ name }']") &.[]('content') &.strip &.presence } duration = content.call('og:video:duration') || content.call('video:duration') published = content.call('article:published_time') || content.call('date') created_range = original_created_range(published) platform_tags = platform_tags(uri) { title: metadata[:title], thumbnail_base: Preview::KnownSiteExtractor.thumbnail_url(uri) || metadata[:image_url], original_created_from: serialise_time(created_range&.first), original_created_before: serialise_time(created_range&.last), duration: duration&.to_f&.then { _1.positive? ? (_1 * 1_000).round : nil }, tags: platform_tags.join(' ') } end def self.platform_tags uri return ['動画', 'YouTube'] if Preview::KnownSiteExtractor.youtube_video_id(uri) return ['動画', 'ニコニコ'] if Preview::KnownSiteExtractor.niconico_video_id(uri) [] end def self.original_created_range value return nil if value.blank? raw = value.to_s.strip from, before = case raw when /\A(\d{4})\z/ year = Regexp.last_match(1).to_i from = Time.zone.local(year, 1, 1) [from, from + 1.year] when /\A(\d{4})-(\d{2})\z/ year = Regexp.last_match(1).to_i month = Regexp.last_match(2).to_i from = Time.zone.local(year, month, 1) [from, from + 1.month] when /\A(\d{4})-(\d{2})-(\d{2})\z/ year = Regexp.last_match(1).to_i month = Regexp.last_match(2).to_i day = Regexp.last_match(3).to_i from = Time.zone.local(year, month, day) [from, from + 1.day] else parse_timestamp_range(raw) end return nil if from.nil? || before.nil? [from, before] rescue ArgumentError, TypeError nil end def self.parse_timestamp_range raw match = raw.match(TIMESTAMP_PATTERN) return nil unless match year = match[1].to_i month = match[2].to_i day = match[3].to_i hour = match[4].to_i minute = match[5]&.to_i || 0 second = match[6]&.to_i || 0 fraction = match[7] offset = match[8] whole_second = second + fractional_seconds(fraction) from = if offset.present? Time.new(year, month, day, hour, minute, whole_second, parse_offset(offset)).in_time_zone else Time.zone.local(year, month, day, hour, minute, whole_second) end before = if fraction.present? from + (10**(-fraction.length)) elsif match[6].present? from + 1.second elsif match[5].present? from + 1.minute else from + 1.hour end [from, before] end def self.parse_offset value return '+00:00' if value == 'Z' value.match?(/\A[+-]\d{2}:\d{2}\z/) ? value : "#{ value[0, 3] }:#{ value[3, 2] }" end def self.fractional_seconds value return 0 if value.blank? Rational(value.to_i, 10**value.length) end def self.serialise_time value return nil if value.nil? value.nsec.zero? ? value.iso8601 : value.iso8601(9) end private_class_method :platform_tags, :original_created_range, :parse_timestamp_range, :parse_offset, :fractional_seconds, :serialise_time end