Reviewed-on: #413 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #413 でマージされました.
このコミットが含まれているのは:
@@ -0,0 +1,198 @@
|
||||
require 'time'
|
||||
require 'date'
|
||||
|
||||
class PostMetadataFetcher
|
||||
TIMESTAMP_PATTERN =
|
||||
/\A(\d{4})-(\d{2})-(\d{2})T(\d{2})
|
||||
(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?
|
||||
(Z|[+-]\d{2}:?\d{2})?\z/x
|
||||
|
||||
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: serialise_duration(duration),
|
||||
display_tags: display_tags(platform_tags),
|
||||
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.display_tags names
|
||||
return [] if names.blank?
|
||||
|
||||
existing_tags =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.where(tag_names: { name: names })
|
||||
.index_by(&:name)
|
||||
|
||||
names.map { |name|
|
||||
tag = existing_tags[name]
|
||||
{
|
||||
name: name,
|
||||
category: (tag&.category || 'meta'),
|
||||
section_literals: [] }
|
||||
}
|
||||
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
|
||||
return nil unless month.between?(1, 12)
|
||||
|
||||
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
|
||||
return nil unless Date.valid_date?(year, month, day)
|
||||
|
||||
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]
|
||||
return nil unless valid_timestamp_components?(year, month, day, hour, minute, second)
|
||||
return nil unless valid_offset?(offset)
|
||||
|
||||
nanoseconds = parse_nanoseconds(fraction)
|
||||
timestamp =
|
||||
if offset.present?
|
||||
Time.new(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second + Rational(nanoseconds, 1_000_000_000),
|
||||
parse_offset(offset)).in_time_zone
|
||||
else
|
||||
Time.zone.local(year, month, day, hour, minute, second).change(nsec: nanoseconds)
|
||||
end
|
||||
|
||||
from = timestamp.change(sec: 0, nsec: 0)
|
||||
before =
|
||||
if match[5].nil?
|
||||
from + 1.hour
|
||||
else
|
||||
from + 1.minute
|
||||
end
|
||||
[from, before]
|
||||
end
|
||||
|
||||
def self.parse_nanoseconds value
|
||||
return 0 if value.blank?
|
||||
|
||||
digits = value[0, 9].ljust(9, '0')
|
||||
Integer(digits, 10)
|
||||
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.serialise_duration value
|
||||
seconds = Float(value)
|
||||
return nil unless seconds.positive?
|
||||
|
||||
milliseconds = (seconds * 1_000).round
|
||||
seconds_string = (milliseconds / 1_000.0).to_s
|
||||
seconds_string.end_with?('.0') ? seconds_string.delete_suffix('.0') : seconds_string
|
||||
rescue ArgumentError, TypeError
|
||||
nil
|
||||
end
|
||||
|
||||
def self.valid_timestamp_components? year, month, day, hour, minute, second
|
||||
return false unless Date.valid_date?(year, month, day)
|
||||
return false unless hour.between?(0, 23)
|
||||
return false unless minute.between?(0, 59)
|
||||
return false unless second.between?(0, 59)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def self.valid_offset? value
|
||||
return true if value.nil? || value == 'Z'
|
||||
|
||||
match = value.match(/\A([+-])(\d{2}):?(\d{2})\z/)
|
||||
return false if match.nil?
|
||||
|
||||
hours = match[2].to_i
|
||||
minutes = match[3].to_i
|
||||
hours.between?(0, 23) && minutes.between?(0, 59)
|
||||
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,
|
||||
:display_tags,
|
||||
:original_created_range,
|
||||
:parse_timestamp_range,
|
||||
:parse_nanoseconds,
|
||||
:parse_offset,
|
||||
:serialise_duration,
|
||||
:valid_timestamp_components?,
|
||||
:valid_offset?,
|
||||
:serialise_time
|
||||
end
|
||||
新しい課題から参照
ユーザをブロックする