f1181e8510
Reviewed-on: #413 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
272 行
8.6 KiB
Ruby
272 行
8.6 KiB
Ruby
class PostCreatePlan
|
|
VIDEO_TAG_NAME = '動画'.freeze
|
|
TAGME_TAG_NAME = 'タグ希望'.freeze
|
|
NO_DEERJIKIST_TAG_NAME = 'ニジラー情報不詳'.freeze
|
|
|
|
def initialize attributes:
|
|
@attributes = attributes.symbolize_keys
|
|
@existing_tags_by_name = nil
|
|
end
|
|
|
|
def build!
|
|
direct_tag_specs, tag_sections = parse_direct_tag_specs
|
|
default_tag_specs = build_default_tag_specs(direct_tag_specs)
|
|
snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs)
|
|
preload_existing_tags_by_name!(snapshot_tag_specs.map { _1[:name] })
|
|
validate_new_tag_specs!(snapshot_tag_specs)
|
|
post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs)
|
|
video_ms = normalise_video_ms(snapshot_tag_specs)
|
|
validate_video_sections!(video_ms, tag_sections)
|
|
parent_post_ids = normalise_parent_post_ids
|
|
validate_parent_post_ids!(parent_post_ids)
|
|
|
|
{
|
|
url: @attributes[:url],
|
|
title: @attributes[:title].to_s,
|
|
thumbnail_base: @attributes[:thumbnail_base].presence,
|
|
original_created_from: @attributes[:original_created_from].presence,
|
|
original_created_before: @attributes[:original_created_before].presence,
|
|
tags: serialised_tags(direct_tag_specs, tag_sections),
|
|
display_tags: display_tags(direct_tag_specs, tag_sections),
|
|
duration: @attributes[:duration].to_s,
|
|
video_ms: video_ms,
|
|
parent_post_ids: parent_post_ids.join(' '),
|
|
direct_tag_specs: direct_tag_specs,
|
|
default_tag_specs: default_tag_specs,
|
|
snapshot_tag_specs: snapshot_tag_specs,
|
|
post_tag_specs: post_tag_specs,
|
|
tag_sections: tag_sections,
|
|
normalised_parent_post_ids: parent_post_ids }
|
|
end
|
|
|
|
private
|
|
|
|
def tag_names = @attributes[:tags].to_s.split
|
|
|
|
def parse_direct_tag_specs
|
|
tag_sections = { }
|
|
direct_tag_specs = []
|
|
|
|
tag_names.each do |raw_name|
|
|
tag_name, category, sections = parse_raw_tag_name(raw_name)
|
|
existing_tag = existing_tags_by_name[tag_name]
|
|
raise Tag::NicoTagNormalisationError if existing_tag&.nico?
|
|
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
|
|
|
|
direct_tag_specs << {
|
|
name: tag_name,
|
|
category: (category || existing_tag&.category || 'general').to_sym }
|
|
if sections.present?
|
|
tag_sections[tag_name] ||= []
|
|
tag_sections[tag_name].concat(sections)
|
|
tag_sections[tag_name] = Tag.merge_section_ranges(tag_sections[tag_name])
|
|
tag_sections.delete(tag_name) if tag_sections[tag_name] == [[0, nil]]
|
|
end
|
|
end
|
|
|
|
[merge_tag_specs(direct_tag_specs), tag_sections]
|
|
end
|
|
|
|
def parse_raw_tag_name raw_name
|
|
name = raw_name.to_s
|
|
prefix, category =
|
|
Tag::CATEGORY_PREFIXES.find {
|
|
name.downcase.start_with?(_1[0])
|
|
} || ['', nil]
|
|
name = name.sub(/\A#{ prefix }/i, '')
|
|
|
|
sections = []
|
|
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
|
name = "#{ match[1] }#{ match[4] }"
|
|
next if match[2].empty? && match[3].empty?
|
|
|
|
sections << Tag.normalise_section_range!(
|
|
begin_raw: match[2],
|
|
end_raw: match[3],
|
|
tag_name: name)
|
|
end
|
|
if name.include?('[') || name.include?(']')
|
|
raise Tag::SectionLiteralParseError.new(raw_name, raw_name)
|
|
end
|
|
|
|
[resolved_tag_name(name), category&.to_sym, sections]
|
|
end
|
|
|
|
def build_default_tag_specs direct_tag_specs
|
|
default_tag_specs = []
|
|
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
|
|
default_tag_specs << {
|
|
name: TAGME_TAG_NAME,
|
|
category: :meta }
|
|
end
|
|
if direct_tag_specs.none? { deerjikist_tag_spec?(_1) }
|
|
default_tag_specs << {
|
|
name: NO_DEERJIKIST_TAG_NAME,
|
|
category: :meta }
|
|
end
|
|
|
|
default_tag_specs
|
|
end
|
|
|
|
def validate_new_tag_specs! specs
|
|
Array(specs).each do |spec|
|
|
next if existing_tags_by_name.key?(spec[:name])
|
|
|
|
validate_new_tag_spec!(spec)
|
|
end
|
|
end
|
|
|
|
def validate_new_tag_spec! spec
|
|
tag_name = TagName.new(name: spec[:name])
|
|
tag = Tag.new(category: spec[:category], tag_name:)
|
|
return if tag_name.valid? && tag.valid?
|
|
|
|
post = Post.new
|
|
tag_name.errors[:name].each do |message|
|
|
post.errors.add :tags, "#{ spec[:name] }: #{ message }"
|
|
end
|
|
tag.errors.each do |error|
|
|
next if error.attribute == :tag_name
|
|
|
|
post.errors.add :tags, "#{ spec[:name] }: #{ error.message }"
|
|
end
|
|
raise ActiveRecord::RecordInvalid, post
|
|
end
|
|
|
|
def expand_parent_tag_specs snapshot_tag_specs
|
|
existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
|
|
expanded_parent_specs =
|
|
Tag.expand_parent_tags(existing_snapshot_tags)
|
|
.reject(&:deprecated?)
|
|
.map { |tag|
|
|
{
|
|
name: tag.name,
|
|
category: tag.category.to_sym } }
|
|
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
|
end
|
|
|
|
def merge_tag_specs specs
|
|
specs.each_with_object({ }) do |spec, merged|
|
|
merged[spec[:name]] =
|
|
if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general
|
|
merged[spec[:name]]
|
|
else
|
|
{
|
|
name: spec[:name],
|
|
category: spec[:category] }
|
|
end
|
|
end.values.sort_by { _1[:name] }
|
|
end
|
|
|
|
def existing_tags_by_name
|
|
@existing_tags_by_name ||= begin
|
|
names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
|
|
Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
|
|
end
|
|
end
|
|
|
|
def preload_existing_tags_by_name! names
|
|
wanted_names = Array(names).map { _1.to_s }.reject(&:blank?).uniq
|
|
missing_names = wanted_names - existing_tags_by_name.keys
|
|
return if missing_names.empty?
|
|
|
|
existing_tags_by_name.merge!(
|
|
Tag.joins(:tag_name)
|
|
.where(tag_names: { name: missing_names })
|
|
.index_by(&:name))
|
|
end
|
|
|
|
def canonical_tag_name_without_sections raw_name
|
|
name, = parse_raw_tag_name(raw_name)
|
|
name
|
|
end
|
|
|
|
def deerjikist_tag_spec? spec
|
|
return true if spec[:category] == :deerjikist
|
|
|
|
existing_tags_by_name[spec[:name]]&.deerjikist?
|
|
end
|
|
|
|
def normalise_parent_post_ids
|
|
Array(@attributes[:parent_post_ids]).flat_map { _1.to_s.split }.map { |token|
|
|
id = Integer(token, exception: false)
|
|
raise ArgumentError, "親投稿 Id. が不正です: #{ token }" if id.nil? || id <= 0
|
|
|
|
id
|
|
}.uniq.sort
|
|
end
|
|
|
|
def validate_parent_post_ids! ids
|
|
missing = ids - Post.where(id: ids).pluck(:id)
|
|
raise ArgumentError, "存在しない親投稿 Id. があります: #{ missing.join(' ') }" if missing.present?
|
|
end
|
|
|
|
def serialised_tags direct_tag_specs, tag_sections
|
|
direct_tag_specs.map { |spec|
|
|
"#{ spec[:name] }#{ tag_sections[spec[:name]].to_a.map { section_literal(_1) }.join }"
|
|
}.sort.join(' ')
|
|
end
|
|
|
|
def display_tags direct_tag_specs, tag_sections
|
|
direct_tag_specs.map { |spec|
|
|
{
|
|
name: spec[:name],
|
|
category: spec[:category].to_s,
|
|
section_literals: tag_sections[spec[:name]].to_a.map { section_literal(_1) } }
|
|
}.sort_by { _1[:name] }
|
|
end
|
|
|
|
def section_literal range
|
|
begin_ms, end_ms = range
|
|
"[#{ Post.ms_to_time(begin_ms) }-#{ end_ms ? Post.ms_to_time(end_ms) : '' }]"
|
|
end
|
|
|
|
def normalise_video_ms snapshot_tag_specs
|
|
return nil unless snapshot_tag_specs.any? { _1[:name] == VIDEO_TAG_NAME }
|
|
|
|
video_ms = @attributes[:video_ms]
|
|
if video_ms.present?
|
|
value = Integer(video_ms, exception: false)
|
|
raise PostCreator::VideoMsParseError unless value&.positive?
|
|
|
|
return value
|
|
end
|
|
|
|
duration = @attributes[:duration]
|
|
return nil if duration.blank?
|
|
|
|
value = Tag.time_to_ms!(duration.to_s, tag_name: '動画時間')
|
|
raise PostCreator::VideoMsParseError unless value.positive?
|
|
|
|
value
|
|
rescue Tag::SectionLiteralParseError
|
|
raise PostCreator::VideoMsParseError
|
|
end
|
|
|
|
def validate_video_sections! video_ms, tag_sections
|
|
return unless video_ms
|
|
|
|
tag_sections.each_value do |ranges|
|
|
ranges.each do |begin_ms, end_ms|
|
|
if begin_ms >= video_ms
|
|
post = Post.new
|
|
post.errors.add :video_ms, 'タグ区間の開始が動画時間以上です.'
|
|
raise ActiveRecord::RecordInvalid, post
|
|
end
|
|
if end_ms && end_ms > video_ms
|
|
post = Post.new
|
|
post.errors.add :video_ms, 'タグ区間の終端が動画時間を超えてゐます.'
|
|
raise ActiveRecord::RecordInvalid, post
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def resolved_tag_name name
|
|
tag_name = TagName.includes(:canonical).find_by(name:)
|
|
return name if tag_name.nil?
|
|
|
|
(tag_name.canonical || tag_name).name
|
|
end
|
|
end
|