このコミットが含まれているのは:
2026-07-16 20:05:09 +09:00
コミット 90d8d3ff08
27個のファイルの変更725行の追加257行の削除
+68 -7
ファイルの表示
@@ -1,4 +1,5 @@
class Post < ApplicationRecord
require 'date'
require 'mini_magick'
require 'stringio'
@@ -237,15 +238,43 @@ class Post < ApplicationRecord
value = raw_value.to_s.strip
return nil if value.blank?
if value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}\z/)
return Time.zone.local(value[0, 4].to_i,
value[5, 2].to_i,
value[8, 2].to_i,
value[11, 2].to_i,
value[14, 2].to_i)
if (match = value.match(/\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})\z/))
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
return nil unless valid_original_created_components?(year, month, day, hour, minute, 0)
return Time.zone.local(year, month, day, hour, minute)
end
Time.iso8601(value).in_time_zone
match =
value.match(
/\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/ \
'(?::(\d{2})(?:\.(\d+))?)?' \
'(Z|[+-]\d{2}:?\d{2})\z/')
return nil if match.nil?
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
second = match[6]&.to_i || 0
fraction = match[7]
offset = match[8]
return nil unless valid_original_created_components?(year, month, day, hour, minute, second)
return nil unless valid_original_created_offset?(offset)
Time.new(
year,
month,
day,
hour,
minute,
second + Rational(parse_original_created_nanoseconds(fraction), 1_000_000_000),
normalise_original_created_offset(offset)).in_time_zone
rescue ArgumentError, TypeError
nil
end
@@ -254,6 +283,38 @@ class Post < ApplicationRecord
value.sec.zero? && value.nsec.zero?
end
def valid_original_created_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 valid_original_created_offset? value
match = value.match(/\A([+-])(\d{2}):?(\d{2})\z/)
return true if value == '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 parse_original_created_nanoseconds value
return 0 if value.blank?
digits = value[0, 9].ljust(9, '0')
Integer(digits, 10)
end
def normalise_original_created_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 skip_original_created_validation?
return false if new_record?
return false if will_save_change_to_original_created_from?
-2
ファイルの表示
@@ -75,8 +75,6 @@ class PostCreator
duration = @attributes[:duration]
return nil if duration.blank?
return duration.to_i if duration.is_a?(Numeric) && duration.to_i.positive?
value = Tag.time_to_ms!(duration.to_s, tag_name: '動画時間')
raise VideoMsParseError unless value.positive?
+23 -4
ファイルの表示
@@ -157,7 +157,15 @@ class PostImportPreviewer
def initial_attributes row
attributes = row[:attributes]&.stringify_keys || { }
FIELDS.to_h { |field| [field, attributes[field].to_s] }
FIELDS.to_h { |field|
value = attributes[field]
normalised =
if field == 'duration'
normalise_duration_attribute(value)
else
value.to_s
end
[field, normalised] }
end
def initial_provenance row
@@ -256,9 +264,20 @@ class PostImportPreviewer
end
def sanitise_metadata_duration value
return nil unless value.is_a?(Numeric)
return nil unless value.is_a?(String)
value.positive? ? value.to_i : nil
value.presence
end
def normalise_duration_attribute value
return '' if value.nil?
return value if value.is_a?(String)
milliseconds = Integer(value, exception: false)
return value.to_s if milliseconds.nil? || milliseconds <= 0
seconds_string = (milliseconds / 1_000.0).to_s
seconds_string.end_with?('.0') ? seconds_string.delete_suffix('.0') : seconds_string
end
def preload_metadata! prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts
@@ -459,7 +478,7 @@ class PostImportPreviewer
def parse_duration value, errors
return nil if value.blank?
value.is_a?(Numeric) ? value.to_i : Tag.time_to_ms!(value.to_s, tag_name: '動画時間')
Tag.time_to_ms!(value.to_s, tag_name: '動画時間')
rescue Tag::SectionLiteralParseError
errors[:video_ms] = ['動画時間の記法が不正です.']
nil
+2 -1
ファイルの表示
@@ -5,9 +5,10 @@ class PostImportRowNormaliser
'thumbnail_base',
'original_created_from',
'original_created_before',
'duration',
'tags',
'parent_post_ids'].freeze
FLEXIBLE_FIELDS = ['duration', 'video_ms'].freeze
FLEXIBLE_FIELDS = ['video_ms'].freeze
ATTRIBUTE_FIELDS = (STRING_FIELDS + FLEXIBLE_FIELDS).freeze
def self.normalise! rows, allow_warning_fields: false
+43 -1
ファイルの表示
@@ -1,4 +1,5 @@
require 'time'
require 'date'
class PostMetadataFetcher
TIMESTAMP_PATTERN =
@@ -30,7 +31,7 @@ class PostMetadataFetcher
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 },
duration: serialise_duration(duration),
tags: platform_tags.join(' ') }
end
@@ -54,12 +55,16 @@ class PostMetadataFetcher
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
@@ -84,6 +89,9 @@ class PostMetadataFetcher
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?
@@ -117,6 +125,37 @@ class PostMetadataFetcher
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?
@@ -128,5 +167,8 @@ class PostMetadataFetcher
:parse_timestamp_range,
:parse_nanoseconds,
:parse_offset,
:serialise_duration,
:valid_timestamp_components?,
:valid_offset?,
:serialise_time
end