class PostImportPreviewer FIELDS = %w[title thumbnail_base original_created_from original_created_before duration tags parent_post_ids].freeze def initialize parsed:, url_column:, mappings: { }, existing: 'skip' @parsed = parsed @url_column = url_column.to_i @mappings = mappings.to_h.stringify_keys @existing = existing == 'error' ? 'error' : 'skip' end def preview urls = { } @parsed[:rows].map do |row| values = row[:values] attributes = FIELDS.to_h { |field| [field, mapped_value(field, values)] } url = values[@url_column].to_s.strip metadata, warnings = fetch_metadata(url) attributes = metadata.merge(attributes) { |_key, automatic, input| input.presence || automatic } post = Post.new(url:) post.valid? normal_url = post.url errors = post.errors.to_hash.transform_values(&:dup) errors[:url] = ['URL が重複しています.'] if normal_url.present? && urls.key?(normal_url) urls[normal_url] = true if normal_url.present? if normal_url.present? && Post.exists?(url: normal_url) @existing == 'error' ? errors[:url] = ['既存投稿です.'] : warnings << '既存投稿のためスキップします.' end validate_preview_tags(attributes['tags'], errors) validate_parents(attributes['parent_post_ids'], errors) { source_row: row[:source_row], url: normal_url || url, attributes:, warnings:, errors:, status: errors.present? ? 'error' : (warnings.present? ? 'warning' : 'ready'), existing: normal_url.present? && Post.exists?(url: normal_url) } end end private def mapped_value field, values mapping = @mappings[field] return '' if mapping.blank? return mapping['value'].to_s if mapping['kind'] == 'fixed' Array(mapping['columns']).filter_map { |index| values[index.to_i].to_s.presence }.join(field == 'tags' ? ' ' : '') end def fetch_metadata url return [{ }, ['URL が空です.']] if url.blank? data = PostMetadataFetcher.fetch(url) warnings = [] warnings << 'タイトルを取得できませんでした.' if data[:title].blank? warnings << 'サムネールを取得できませんでした.' if data[:thumbnail_base].blank? [data.compact, warnings] rescue Preview::UrlSafety::UnsafeUrl, Preview::HttpFetcher::FetchFailed, Preview::HttpFetcher::ResponseTooLarge => e [{ }, ["メタデータを取得できませんでした: #{ e.message }"]] end def validate_preview_tags raw, errors names = raw.to_s.split return if names.empty? if names.any? { _1.downcase.start_with?('nico:') } errors[:tags] = ['ニコニコ・タグは直接指定できません.'] return end canonical = names.map { TagName.canonicalise(_1.sub(/\[.*\]\z/, '')).first } if Tag.joins(:tag_name).where(tag_names: { name: canonical }, deprecated_at: nil).count < canonical.uniq.length errors[:tags] = ['存在しないタグ又は廃止済みタグがあります.'] end rescue Tag::SectionLiteralParseError errors[:tags] = ['タグ区間の記法が不正です.'] end def validate_parents raw, errors ids = raw.to_s.split.map { Integer(_1, exception: false) } return if ids.compact.empty? && raw.to_s.blank? errors[:parent_post_ids] = ['親投稿 Id. が不正です.'] and return if ids.any? { _1.nil? || _1 <= 0 } errors[:parent_post_ids] = ['存在しない親投稿 Id. があります.'] if Post.where(id: ids).count != ids.uniq.length end end